Files
SnapOtter/tests/qa/settings.qa.spec.ts
T
SnapOtterandGitHub 19d9ed181a fix(audio): low-samplerate ogg encode + post-2.0 QA hygiene
Quality-VBR ogg (libvorbis -q:a) fixes 8 kHz 'encoder setup failed' in both ogg paths; drop empty COOKIE_SECRET ENV (app auto-generates); emit real bundle extractedSize; fix stale image-pad/compress-pdf QA specs.
2026-06-20 10:41:50 +08:00

2157 lines
64 KiB
TypeScript

/**
* SETTINGS-SEMANTIC QA sweep: verify every tool setting actually changes the
* output in the expected way, not just "no crash".
*
* For each tool, we vary one setting at a time from a sane default, process,
* download, and assert the output changed as expected via decode oracles
* (imageInfo, imageLuma, imageSaturation, probeMedia, mediaDuration, magicMatches).
*
* Run:
* pnpm playwright test --config tests/qa/playwright.qa.config.ts tests/qa/settings.qa.spec.ts --workers=1
*/
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { expect, type Page, test } from "@playwright/test";
import {
downloadResult,
fixture,
gotoTool,
imageInfo,
imageLuma,
imageSaturation,
instrument,
isClean,
issuesSummary,
magicMatches,
mediaDuration,
type PageIssues,
probeMedia,
processTool,
signalStats,
uploadFiles,
} from "./qa-helpers";
// ── Findings accumulator ─────────────────────────────────────────────
interface Finding {
tool: string;
setting: string;
value: string;
expected: string;
actual: string;
severity: "bug" | "warn";
}
const findings: Finding[] = [];
const coverage: Record<string, { tested: number; total: number }> = {};
function bug(f: Omit<Finding, "severity">) {
findings.push({ ...f, severity: "bug" });
}
function warn(f: Omit<Finding, "severity">) {
findings.push({ ...f, severity: "warn" });
}
// ── Fixtures ─────────────────────────────────────────────────────────
const IMG_200x150 = fixture("test-200x150.png"); // 200x150 PNG
const IMG_JPG = fixture("sample-photo.jpg");
const VID_MP4 = fixture("media", "tiny.mp4"); // 64x64, 1s, 8fps
const AUD_MP3 = fixture("media", "tiny.mp3"); // mono, ~1s
const AUD_STEREO = fixture("media", "tone-stereo.wav"); // stereo, 1s
const PDF_3PAGE = fixture("test-3page.pdf"); // 3 pages
// ── Helpers ──────────────────────────────────────────────────────────
const TOOL_TIMEOUT = 120_000;
/** Fill a numeric input by id, clearing first. */
async function fillInput(page: Page, id: string, value: string | number) {
const input = page.locator(`#${id}`);
await input.waitFor({ state: "visible", timeout: 10_000 });
await input.fill(String(value));
}
/** Set a slider by id to a specific value. */
async function setSlider(page: Page, id: string, value: number) {
const slider = page.locator(`#${id}`);
await slider.waitFor({ state: "visible", timeout: 10_000 });
await slider.fill(String(value));
}
/** Select a value in a <select> by id. */
async function selectOption(page: Page, id: string, value: string) {
const sel = page.locator(`#${id}`);
await sel.waitFor({ state: "visible", timeout: 10_000 });
await sel.selectOption(value);
}
/** Click a button matching text (for tab buttons, effect buttons, etc). */
async function clickButton(page: Page, text: string | RegExp) {
await page.getByRole("button", { name: text }).first().click();
}
/** Process and download for a specific tool, returning the downloaded file info. */
async function processAndDownload(page: Page, toolId: string, hint: "fast" | "long" = "fast") {
const result = await processTool(page, toolId, hint);
if (!result.ok) {
return { ok: false as const, error: result.error, path: "", size: 0, buf: Buffer.alloc(0) };
}
const dl = await downloadResult(page, toolId);
return { ok: true as const, ...dl };
}
/** Navigate to tool, upload a file, wait for it to be ready. */
async function setupTool(page: Page, toolId: string, file: string) {
await gotoTool(page, toolId);
await uploadFiles(page, file);
// Wait for the submit button to be enabled (file loaded)
const submit = page.getByTestId(`${toolId}-submit`);
await submit.first().waitFor({ state: "visible", timeout: 20_000 });
}
// ── Write findings report ────────────────────────────────────────────
test.afterAll(async () => {
const reportPath = path.join(__dirname, "..", "..", "docs", "qa", "findings-settings.md");
const lines: string[] = [
"# Settings-Semantic QA Findings",
"",
`Generated: ${new Date().toISOString()}`,
"",
];
if (findings.length === 0) {
lines.push("## No bugs found", "", "All tested settings produced the expected output changes.");
} else {
const bugs = findings.filter((f) => f.severity === "bug");
const warns = findings.filter((f) => f.severity === "warn");
lines.push(`## Summary: ${bugs.length} bug(s), ${warns.length} warning(s)`, "");
lines.push("## Bugs", "");
lines.push("| Tool | Setting | Value | Expected | Actual |");
lines.push("|------|---------|-------|----------|--------|");
for (const f of bugs) {
lines.push(`| ${f.tool} | ${f.setting} | ${f.value} | ${f.expected} | ${f.actual} |`);
}
if (warns.length) {
lines.push("", "## Warnings", "");
lines.push("| Tool | Setting | Value | Expected | Actual |");
lines.push("|------|---------|-------|----------|--------|");
for (const f of warns) {
lines.push(`| ${f.tool} | ${f.setting} | ${f.value} | ${f.expected} | ${f.actual} |`);
}
}
}
lines.push("", "## Per-Tool Coverage", "");
lines.push("| Tool | Settings Tested | Total Settings |");
lines.push("|------|----------------|----------------|");
for (const [tool, cov] of Object.entries(coverage).sort((a, b) => a[0].localeCompare(b[0]))) {
lines.push(`| ${tool} | ${cov.tested} | ${cov.total} |`);
}
fs.mkdirSync(path.dirname(reportPath), { recursive: true });
fs.writeFileSync(reportPath, lines.join("\n") + "\n");
});
// =====================================================================
// IMAGE TOOLS
// =====================================================================
test.describe("IMAGE: resize", () => {
test.setTimeout(TOOL_TIMEOUT);
test("width=100 -> output width 100", async ({ page }) => {
const issues = instrument(page);
await setupTool(page, "resize", IMG_200x150);
await fillInput(page, "resize-width", 100);
const dl = await processAndDownload(page, "resize");
if (!dl.ok) {
bug({
tool: "resize",
setting: "width",
value: "100",
expected: "output width=100",
actual: dl.error ?? "error",
});
return;
}
const info = imageInfo(dl.path);
if (info.width !== 100)
bug({
tool: "resize",
setting: "width",
value: "100",
expected: "width=100",
actual: `width=${info.width}`,
});
expect(info.width).toBe(100);
});
test("height=50 -> output height 50", async ({ page }) => {
const issues = instrument(page);
await setupTool(page, "resize", IMG_200x150);
await fillInput(page, "resize-height", 50);
const dl = await processAndDownload(page, "resize");
if (!dl.ok) {
bug({
tool: "resize",
setting: "height",
value: "50",
expected: "output height=50",
actual: dl.error ?? "error",
});
return;
}
const info = imageInfo(dl.path);
if (info.height !== 50)
bug({
tool: "resize",
setting: "height",
value: "50",
expected: "height=50",
actual: `height=${info.height}`,
});
expect(info.height).toBe(50);
});
test("percentage=25 via scale tab -> output ~50x38", async ({ page }) => {
const issues = instrument(page);
await setupTool(page, "resize", IMG_200x150);
// Click scale tab
await page.getByRole("button", { name: /scale/i }).first().click();
await fillInput(page, "resize-scale", 25);
const dl = await processAndDownload(page, "resize");
if (!dl.ok) {
bug({
tool: "resize",
setting: "percentage",
value: "25",
expected: "~50x38",
actual: dl.error ?? "error",
});
return;
}
const info = imageInfo(dl.path);
// 25% of 200x150 = 50x37.5 -> 50x38
if (info.width !== 50)
bug({
tool: "resize",
setting: "percentage",
value: "25",
expected: "width=50",
actual: `width=${info.width}`,
});
expect(info.width).toBe(50);
});
test("fit=contain with both dims -> output fits inside box", async ({ page }) => {
const issues = instrument(page);
await setupTool(page, "resize", IMG_200x150);
await fillInput(page, "resize-width", 80);
await fillInput(page, "resize-height", 80);
// Click contain
await page
.getByRole("button", { name: /fit inside/i })
.first()
.click();
const dl = await processAndDownload(page, "resize");
if (!dl.ok) {
bug({
tool: "resize",
setting: "fit",
value: "contain",
expected: "fits inside 80x80",
actual: dl.error ?? "error",
});
return;
}
const info = imageInfo(dl.path);
// contain: max dim is 80, aspect 200/150=1.33, so 80x60
if (info.width > 80 || info.height > 80)
bug({
tool: "resize",
setting: "fit",
value: "contain",
expected: "max dim <= 80",
actual: `${info.width}x${info.height}`,
});
expect(info.width).toBeLessThanOrEqual(80);
expect(info.height).toBeLessThanOrEqual(80);
});
test("fit=cover with both dims -> output covers box", async ({ page }) => {
const issues = instrument(page);
await setupTool(page, "resize", IMG_200x150);
await fillInput(page, "resize-width", 80);
await fillInput(page, "resize-height", 80);
// Click crop to fit (cover)
await page
.getByRole("button", { name: /crop to fit/i })
.first()
.click();
const dl = await processAndDownload(page, "resize");
if (!dl.ok) {
bug({
tool: "resize",
setting: "fit",
value: "cover",
expected: "exactly 80x80",
actual: dl.error ?? "error",
});
return;
}
const info = imageInfo(dl.path);
if (info.width !== 80 || info.height !== 80)
bug({
tool: "resize",
setting: "fit",
value: "cover",
expected: "80x80",
actual: `${info.width}x${info.height}`,
});
expect(info.width).toBe(80);
expect(info.height).toBe(80);
});
test("fit=fill (stretch) with both dims -> exactly WxH", async ({ page }) => {
const issues = instrument(page);
await setupTool(page, "resize", IMG_200x150);
await fillInput(page, "resize-width", 80);
await fillInput(page, "resize-height", 60);
await page
.getByRole("button", { name: /stretch/i })
.first()
.click();
const dl = await processAndDownload(page, "resize");
if (!dl.ok) {
bug({
tool: "resize",
setting: "fit",
value: "fill",
expected: "80x60",
actual: dl.error ?? "error",
});
return;
}
const info = imageInfo(dl.path);
if (info.width !== 80 || info.height !== 60)
bug({
tool: "resize",
setting: "fit",
value: "fill",
expected: "80x60",
actual: `${info.width}x${info.height}`,
});
expect(info.width).toBe(80);
expect(info.height).toBe(60);
});
coverage.resize = { tested: 6, total: 6 };
});
test.describe("IMAGE: crop", () => {
test.setTimeout(TOOL_TIMEOUT);
test("crop region -> output dims match crop size", async ({ page }) => {
const issues = instrument(page);
await gotoTool(page, "crop");
await uploadFiles(page, IMG_200x150);
// Wait for interactive crop UI to load, then set numeric values
const cropW = page.locator("#crop-width");
await cropW.waitFor({ state: "visible", timeout: 20_000 });
await page.locator("#crop-x").fill("10");
await page.locator("#crop-y").fill("10");
await cropW.fill("80");
await page.locator("#crop-height").fill("60");
const dl = await processAndDownload(page, "crop");
if (!dl.ok) {
bug({
tool: "crop",
setting: "region",
value: "10,10,80,60",
expected: "80x60",
actual: dl.error ?? "error",
});
return;
}
const info = imageInfo(dl.path);
if (info.width !== 80 || info.height !== 60)
bug({
tool: "crop",
setting: "region",
value: "10,10,80,60",
expected: "80x60",
actual: `${info.width}x${info.height}`,
});
expect(info.width).toBe(80);
expect(info.height).toBe(60);
});
coverage.crop = { tested: 1, total: 2 };
});
test.describe("IMAGE: rotate", () => {
test.setTimeout(TOOL_TIMEOUT);
test("90 degrees CW -> dims swap (200x150 becomes 150x200)", async ({ page }) => {
const issues = instrument(page);
await setupTool(page, "rotate", IMG_200x150);
await page.getByTestId("rotate-right").click();
const dl = await processAndDownload(page, "rotate");
if (!dl.ok) {
bug({
tool: "rotate",
setting: "angle",
value: "90",
expected: "150x200",
actual: dl.error ?? "error",
});
return;
}
const info = imageInfo(dl.path);
if (info.width !== 150 || info.height !== 200)
bug({
tool: "rotate",
setting: "angle",
value: "90",
expected: "150x200",
actual: `${info.width}x${info.height}`,
});
expect(info.width).toBe(150);
expect(info.height).toBe(200);
});
test("180 degrees -> same dims, different content", async ({ page }) => {
const issues = instrument(page);
await setupTool(page, "rotate", IMG_200x150);
await page.getByRole("button", { name: "180" }).first().click();
const dl = await processAndDownload(page, "rotate");
if (!dl.ok) {
bug({
tool: "rotate",
setting: "angle",
value: "180",
expected: "200x150",
actual: dl.error ?? "error",
});
return;
}
const info = imageInfo(dl.path);
if (info.width !== 200 || info.height !== 150)
bug({
tool: "rotate",
setting: "angle",
value: "180",
expected: "200x150",
actual: `${info.width}x${info.height}`,
});
expect(info.width).toBe(200);
expect(info.height).toBe(150);
});
test("270 degrees -> dims swap", async ({ page }) => {
const issues = instrument(page);
await setupTool(page, "rotate", IMG_200x150);
await page.getByTestId("rotate-left").click();
const dl = await processAndDownload(page, "rotate");
if (!dl.ok) {
bug({
tool: "rotate",
setting: "angle",
value: "270",
expected: "150x200",
actual: dl.error ?? "error",
});
return;
}
const info = imageInfo(dl.path);
if (info.width !== 150 || info.height !== 200)
bug({
tool: "rotate",
setting: "angle",
value: "270",
expected: "150x200",
actual: `${info.width}x${info.height}`,
});
expect(info.width).toBe(150);
expect(info.height).toBe(200);
});
test("flip horizontal -> same dims, runs without error", async ({ page }) => {
const issues = instrument(page);
await setupTool(page, "rotate", IMG_200x150);
await page.getByTestId("rotate-flip-h").click();
const dl = await processAndDownload(page, "rotate");
if (!dl.ok)
bug({
tool: "rotate",
setting: "flipH",
value: "true",
expected: "success",
actual: dl.error ?? "error",
});
expect(dl.ok).toBe(true);
});
test("flip vertical -> same dims, runs without error", async ({ page }) => {
const issues = instrument(page);
await setupTool(page, "rotate", IMG_200x150);
await page.getByTestId("rotate-flip-v").click();
const dl = await processAndDownload(page, "rotate");
if (!dl.ok)
bug({
tool: "rotate",
setting: "flipV",
value: "true",
expected: "success",
actual: dl.error ?? "error",
});
expect(dl.ok).toBe(true);
});
coverage.rotate = { tested: 5, total: 5 };
});
test.describe("IMAGE: convert", () => {
test.setTimeout(TOOL_TIMEOUT);
for (const fmt of ["jpg", "png", "webp", "avif", "tiff"] as const) {
test(`format=${fmt} -> output magic bytes match`, async ({ page }) => {
const issues = instrument(page);
await setupTool(page, "convert", IMG_200x150);
await selectOption(page, "convert-target-format", fmt);
const dl = await processAndDownload(page, "convert");
if (!dl.ok) {
bug({
tool: "convert",
setting: "format",
value: fmt,
expected: `${fmt} magic bytes`,
actual: dl.error ?? "error",
});
return;
}
const matches = magicMatches(dl.buf, fmt);
if (!matches)
bug({
tool: "convert",
setting: "format",
value: fmt,
expected: "correct magic bytes",
actual: "magic mismatch",
});
expect(matches).toBe(true);
});
}
coverage.convert = { tested: 5, total: 6 };
});
test.describe("IMAGE: compress", () => {
test.setTimeout(TOOL_TIMEOUT);
test("quality mode low (10) -> smaller than quality high (95)", async ({ page }) => {
const issues = instrument(page);
// High quality
await setupTool(page, "compress", IMG_JPG);
await page
.getByRole("button", { name: /quality/i })
.first()
.click();
await setSlider(page, "compress-quality", 95);
const dlHigh = await processAndDownload(page, "compress");
// Low quality - fresh page
await setupTool(page, "compress", IMG_JPG);
await page
.getByRole("button", { name: /quality/i })
.first()
.click();
await setSlider(page, "compress-quality", 10);
const dlLow = await processAndDownload(page, "compress");
if (!dlHigh.ok || !dlLow.ok) {
bug({
tool: "compress",
setting: "quality",
value: "10 vs 95",
expected: "both succeed",
actual: `high:${dlHigh.ok} low:${dlLow.ok}`,
});
return;
}
if (dlLow.size >= dlHigh.size)
bug({
tool: "compress",
setting: "quality",
value: "10 vs 95",
expected: "low quality smaller",
actual: `low=${dlLow.size} high=${dlHigh.size}`,
});
expect(dlLow.size).toBeLessThan(dlHigh.size);
});
test("targetSize=50KB -> output roughly <= 50KB", async ({ page }) => {
const issues = instrument(page);
await setupTool(page, "compress", IMG_JPG);
// targetSize mode is default
await fillInput(page, "compress-target-size", 50);
const dl = await processAndDownload(page, "compress");
if (!dl.ok) {
bug({
tool: "compress",
setting: "targetSize",
value: "50KB",
expected: "~50KB",
actual: dl.error ?? "error",
});
return;
}
// Allow 20% tolerance
const sizeKb = dl.size / 1024;
if (sizeKb > 65)
bug({
tool: "compress",
setting: "targetSize",
value: "50KB",
expected: "<=65KB (50+20%)",
actual: `${sizeKb.toFixed(1)}KB`,
});
});
coverage.compress = { tested: 2, total: 3 };
});
test.describe("IMAGE: adjust-colors", () => {
test.setTimeout(TOOL_TIMEOUT);
test("brightness +80 -> luma increases", async ({ page }) => {
const issues = instrument(page);
// Baseline
const baseStats = signalStats(IMG_200x150);
await setupTool(page, "adjust-colors", IMG_200x150);
await setSlider(page, "color-slider-brightness", 80);
const dl = await processAndDownload(page, "adjust-colors");
if (!dl.ok) {
bug({
tool: "adjust-colors",
setting: "brightness",
value: "+80",
expected: "luma increases",
actual: dl.error ?? "error",
});
return;
}
const luma = imageLuma(dl.path);
if (luma <= baseStats.luma)
bug({
tool: "adjust-colors",
setting: "brightness",
value: "+80",
expected: `luma > ${baseStats.luma}`,
actual: `luma=${luma}`,
});
expect(luma).toBeGreaterThan(baseStats.luma);
});
test("brightness -80 -> luma decreases", async ({ page }) => {
const issues = instrument(page);
const baseStats = signalStats(IMG_200x150);
await setupTool(page, "adjust-colors", IMG_200x150);
await setSlider(page, "color-slider-brightness", -80);
const dl = await processAndDownload(page, "adjust-colors");
if (!dl.ok) {
bug({
tool: "adjust-colors",
setting: "brightness",
value: "-80",
expected: "luma decreases",
actual: dl.error ?? "error",
});
return;
}
const luma = imageLuma(dl.path);
if (luma >= baseStats.luma)
bug({
tool: "adjust-colors",
setting: "brightness",
value: "-80",
expected: `luma < ${baseStats.luma}`,
actual: `luma=${luma}`,
});
expect(luma).toBeLessThan(baseStats.luma);
});
test("saturation=-100 -> near-zero saturation (grayscale)", async ({ page }) => {
const issues = instrument(page);
await setupTool(page, "adjust-colors", IMG_200x150);
await setSlider(page, "color-slider-saturation", -100);
const dl = await processAndDownload(page, "adjust-colors");
if (!dl.ok) {
bug({
tool: "adjust-colors",
setting: "saturation",
value: "-100",
expected: "sat~0",
actual: dl.error ?? "error",
});
return;
}
const sat = imageSaturation(dl.path);
if (sat > 15)
bug({
tool: "adjust-colors",
setting: "saturation",
value: "-100",
expected: "saturation ~0 (< 15)",
actual: `sat=${sat}`,
});
expect(sat).toBeLessThan(15);
});
test("grayscale effect -> near-zero saturation", async ({ page }) => {
const issues = instrument(page);
await setupTool(page, "adjust-colors", IMG_200x150);
await page.getByRole("button", { name: "grayscale", exact: true }).first().click();
const dl = await processAndDownload(page, "adjust-colors");
if (!dl.ok) {
bug({
tool: "adjust-colors",
setting: "effect",
value: "grayscale",
expected: "sat~0",
actual: dl.error ?? "error",
});
return;
}
const sat = imageSaturation(dl.path);
if (sat > 15)
bug({
tool: "adjust-colors",
setting: "effect",
value: "grayscale",
expected: "saturation ~0 (< 15)",
actual: `sat=${sat}`,
});
expect(sat).toBeLessThan(15);
});
test("contrast +80 -> output changes (not identical to original)", async ({ page }) => {
const issues = instrument(page);
const baseLuma = imageLuma(IMG_200x150);
await setupTool(page, "adjust-colors", IMG_200x150);
await setSlider(page, "color-slider-contrast", 80);
const dl = await processAndDownload(page, "adjust-colors");
if (!dl.ok) {
bug({
tool: "adjust-colors",
setting: "contrast",
value: "+80",
expected: "output changes",
actual: dl.error ?? "error",
});
return;
}
const outLuma = imageLuma(dl.path);
// Contrast should shift luma away from midpoint, or at least change it
if (outLuma === baseLuma)
warn({
tool: "adjust-colors",
setting: "contrast",
value: "+80",
expected: "luma changes",
actual: `luma unchanged at ${outLuma}`,
});
});
test("invert effect -> luma inverts (~255-original)", async ({ page }) => {
const issues = instrument(page);
const baseLuma = imageLuma(IMG_200x150);
await setupTool(page, "adjust-colors", IMG_200x150);
await page.getByRole("button", { name: "invert", exact: true }).first().click();
const dl = await processAndDownload(page, "adjust-colors");
if (!dl.ok) {
bug({
tool: "adjust-colors",
setting: "effect",
value: "invert",
expected: "luma inverts",
actual: dl.error ?? "error",
});
return;
}
const outLuma = imageLuma(dl.path);
// Inverted luma should be roughly 255 - original (within 30)
const expectedLuma = 255 - baseLuma;
if (Math.abs(outLuma - expectedLuma) > 40)
bug({
tool: "adjust-colors",
setting: "effect",
value: "invert",
expected: `luma ~${expectedLuma}`,
actual: `luma=${outLuma}`,
});
});
coverage["adjust-colors"] = { tested: 6, total: 12 };
});
test.describe("IMAGE: border", () => {
test.setTimeout(TOOL_TIMEOUT);
test("borderWidth=20 -> output dims grow by ~2x20=40", async ({ page }) => {
const issues = instrument(page);
await setupTool(page, "border", IMG_200x150);
await setSlider(page, "border-width", 20);
const dl = await processAndDownload(page, "border");
if (!dl.ok) {
bug({
tool: "border",
setting: "borderWidth",
value: "20",
expected: "dims grow by ~40",
actual: dl.error ?? "error",
});
return;
}
const info = imageInfo(dl.path);
// Border adds to both sides: 200+40=240, 150+40=190
const expectedW = 200 + 2 * 20;
const expectedH = 150 + 2 * 20;
if (Math.abs(info.width - expectedW) > 4 || Math.abs(info.height - expectedH) > 4) {
bug({
tool: "border",
setting: "borderWidth",
value: "20",
expected: `~${expectedW}x${expectedH}`,
actual: `${info.width}x${info.height}`,
});
}
expect(info.width).toBeGreaterThan(200);
expect(info.height).toBeGreaterThan(150);
});
test("borderWidth=0, padding=30 -> output dims grow by ~2x30=60", async ({ page }) => {
const issues = instrument(page);
await setupTool(page, "border", IMG_200x150);
await setSlider(page, "border-width", 0);
await setSlider(page, "border-padding", 30);
const dl = await processAndDownload(page, "border");
if (!dl.ok) {
bug({
tool: "border",
setting: "padding",
value: "30",
expected: "dims grow by ~60",
actual: dl.error ?? "error",
});
return;
}
const info = imageInfo(dl.path);
expect(info.width).toBeGreaterThan(200);
expect(info.height).toBeGreaterThan(150);
});
coverage.border = { tested: 2, total: 5 };
});
test.describe("IMAGE: pixelate", () => {
test.setTimeout(TOOL_TIMEOUT);
test("blockSize=2 (min) -> succeeds, same dims", async ({ page }) => {
const issues = instrument(page);
await setupTool(page, "pixelate", IMG_200x150);
await setSlider(page, "pixelate-block-size", 2);
const dl = await processAndDownload(page, "pixelate");
if (!dl.ok) {
bug({
tool: "pixelate",
setting: "blockSize",
value: "2",
expected: "success",
actual: dl.error ?? "error",
});
return;
}
const info = imageInfo(dl.path);
expect(info.width).toBe(200);
expect(info.height).toBe(150);
});
test("blockSize=64 (large) -> same dims, image is pixelated (different bytes)", async ({
page,
}) => {
const issues = instrument(page);
await setupTool(page, "pixelate", IMG_200x150);
await setSlider(page, "pixelate-block-size", 64);
const dl = await processAndDownload(page, "pixelate");
if (!dl.ok) {
bug({
tool: "pixelate",
setting: "blockSize",
value: "64",
expected: "success",
actual: dl.error ?? "error",
});
return;
}
const info = imageInfo(dl.path);
expect(info.width).toBe(200);
expect(info.height).toBe(150);
});
test("blockSize=128 (max) -> succeeds", async ({ page }) => {
const issues = instrument(page);
await setupTool(page, "pixelate", IMG_200x150);
await setSlider(page, "pixelate-block-size", 128);
const dl = await processAndDownload(page, "pixelate");
if (!dl.ok)
bug({
tool: "pixelate",
setting: "blockSize",
value: "128",
expected: "success",
actual: dl.error ?? "error",
});
expect(dl.ok).toBe(true);
});
coverage.pixelate = { tested: 3, total: 3 };
});
test.describe("IMAGE: vignette", () => {
test.setTimeout(TOOL_TIMEOUT);
test("strength=0.1 (min) -> runs, output slightly darker edges", async ({ page }) => {
const issues = instrument(page);
await setupTool(page, "vignette", IMG_200x150);
await setSlider(page, "vignette-strength", 0.1);
const dl = await processAndDownload(page, "vignette");
if (!dl.ok)
bug({
tool: "vignette",
setting: "strength",
value: "0.1",
expected: "success",
actual: dl.error ?? "error",
});
expect(dl.ok).toBe(true);
});
test("strength=1.0 (max) -> runs, output darker overall", async ({ page }) => {
const issues = instrument(page);
const baseLuma = imageLuma(IMG_200x150);
await setupTool(page, "vignette", IMG_200x150);
await setSlider(page, "vignette-strength", 1.0);
const dl = await processAndDownload(page, "vignette");
if (!dl.ok) {
bug({
tool: "vignette",
setting: "strength",
value: "1.0",
expected: "darker overall",
actual: dl.error ?? "error",
});
return;
}
const outLuma = imageLuma(dl.path);
// Strong vignette should darken overall average
if (outLuma >= baseLuma)
warn({
tool: "vignette",
setting: "strength",
value: "1.0",
expected: `luma < ${baseLuma}`,
actual: `luma=${outLuma}`,
});
});
coverage.vignette = { tested: 2, total: 2 };
});
test.describe("IMAGE: duotone", () => {
test.setTimeout(TOOL_TIMEOUT);
test("duotone -> output has reduced color variety (low saturation)", async ({ page }) => {
const issues = instrument(page);
await setupTool(page, "duotone", IMG_200x150);
// Defaults: shadow=#1e3a8a, highlight=#fbbf24
const dl = await processAndDownload(page, "duotone");
if (!dl.ok) {
bug({
tool: "duotone",
setting: "colors",
value: "default",
expected: "success",
actual: dl.error ?? "error",
});
return;
}
// Duotone maps all colors to a gradient between two colors - should still succeed
const info = imageInfo(dl.path);
expect(info.width).toBe(200);
});
coverage.duotone = { tested: 1, total: 2 };
});
test.describe("IMAGE: circle-crop", () => {
test.setTimeout(TOOL_TIMEOUT);
test("circle-crop -> output has alpha (PNG with transparency)", async ({ page }) => {
const issues = instrument(page);
await setupTool(page, "circle-crop", IMG_200x150);
const dl = await processAndDownload(page, "circle-crop");
if (!dl.ok) {
bug({
tool: "circle-crop",
setting: "default",
value: "default",
expected: "hasAlpha=true",
actual: dl.error ?? "error",
});
return;
}
const info = imageInfo(dl.path);
if (!info.hasAlpha)
bug({
tool: "circle-crop",
setting: "output",
value: "default",
expected: "hasAlpha=true (PNG with transparent corners)",
actual: `hasAlpha=${info.hasAlpha}, pixFmt=${info.pixFmt}`,
});
expect(info.hasAlpha).toBe(true);
});
coverage["circle-crop"] = { tested: 1, total: 1 };
});
test.describe("IMAGE: image-pad", () => {
test.setTimeout(TOOL_TIMEOUT);
test("target=1:1 -> output is square (max dim)", async ({ page }) => {
const issues = instrument(page);
await setupTool(page, "image-pad", IMG_200x150);
await clickButton(page, "1:1");
const dl = await processAndDownload(page, "image-pad");
if (!dl.ok) {
bug({
tool: "image-pad",
setting: "target",
value: "1:1",
expected: "square output",
actual: dl.error ?? "error",
});
return;
}
const info = imageInfo(dl.path);
// 1:1 padding on 200x150 -> 200x200 (pad to max dim)
if (info.width !== info.height)
bug({
tool: "image-pad",
setting: "target",
value: "1:1",
expected: "square (w==h)",
actual: `${info.width}x${info.height}`,
});
expect(info.width).toBe(info.height);
});
test("target=16:9 -> output is 16:9 ratio", async ({ page }) => {
const issues = instrument(page);
await setupTool(page, "image-pad", IMG_200x150);
await clickButton(page, "16:9");
const dl = await processAndDownload(page, "image-pad");
if (!dl.ok) {
bug({
tool: "image-pad",
setting: "target",
value: "16:9",
expected: "16:9 ratio",
actual: dl.error ?? "error",
});
return;
}
const info = imageInfo(dl.path);
const ratio = info.width / info.height;
const expectedRatio = 16 / 9;
if (Math.abs(ratio - expectedRatio) > 0.05)
bug({
tool: "image-pad",
setting: "target",
value: "16:9",
expected: `ratio ~${expectedRatio.toFixed(2)}`,
actual: `ratio=${ratio.toFixed(2)}`,
});
expect(Math.abs(ratio - expectedRatio)).toBeLessThan(0.05);
});
coverage["image-pad"] = { tested: 2, total: 2 };
});
test.describe("IMAGE: watermark-text", () => {
test.setTimeout(TOOL_TIMEOUT);
test("text watermark -> output differs from original (watermark applied)", async ({ page }) => {
const issues = instrument(page);
const baseLuma = imageLuma(IMG_200x150);
await setupTool(page, "watermark-text", IMG_200x150);
// Defaults: text="Sample Watermark", fontSize=48, opacity=50, position=center
const dl = await processAndDownload(page, "watermark-text");
if (!dl.ok) {
bug({
tool: "watermark-text",
setting: "text",
value: "Sample Watermark",
expected: "output changes",
actual: dl.error ?? "error",
});
return;
}
const outLuma = imageLuma(dl.path);
const info = imageInfo(dl.path);
// Dims should be same
expect(info.width).toBe(200);
expect(info.height).toBe(150);
});
test("fontSize=8 (min) -> succeeds", async ({ page }) => {
const issues = instrument(page);
await setupTool(page, "watermark-text", IMG_200x150);
await setSlider(page, "watermark-text-font-size", 8);
const dl = await processAndDownload(page, "watermark-text");
if (!dl.ok)
bug({
tool: "watermark-text",
setting: "fontSize",
value: "8",
expected: "success",
actual: dl.error ?? "error",
});
expect(dl.ok).toBe(true);
});
test("fontSize=200 (max) -> succeeds", async ({ page }) => {
const issues = instrument(page);
await setupTool(page, "watermark-text", IMG_200x150);
await setSlider(page, "watermark-text-font-size", 200);
const dl = await processAndDownload(page, "watermark-text");
if (!dl.ok)
bug({
tool: "watermark-text",
setting: "fontSize",
value: "200",
expected: "success",
actual: dl.error ?? "error",
});
expect(dl.ok).toBe(true);
});
test("position=tiled -> succeeds", async ({ page }) => {
const issues = instrument(page);
await setupTool(page, "watermark-text", IMG_200x150);
await selectOption(page, "watermark-text-position", "tiled");
const dl = await processAndDownload(page, "watermark-text");
if (!dl.ok)
bug({
tool: "watermark-text",
setting: "position",
value: "tiled",
expected: "success",
actual: dl.error ?? "error",
});
expect(dl.ok).toBe(true);
});
test("opacity=0 -> output nearly identical to original (invisible watermark)", async ({
page,
}) => {
const issues = instrument(page);
await setupTool(page, "watermark-text", IMG_200x150);
await setSlider(page, "watermark-text-opacity", 0);
const dl = await processAndDownload(page, "watermark-text");
if (!dl.ok) {
bug({
tool: "watermark-text",
setting: "opacity",
value: "0",
expected: "nearly identical output",
actual: dl.error ?? "error",
});
return;
}
// Just verify it ran
expect(dl.size).toBeGreaterThan(0);
});
coverage["watermark-text"] = { tested: 5, total: 6 };
});
test.describe("IMAGE: sharpening", () => {
test.setTimeout(TOOL_TIMEOUT);
test("adaptive method default -> runs, output is valid image", async ({ page }) => {
const issues = instrument(page);
await setupTool(page, "sharpening", IMG_200x150);
// Default is adaptive/Medium
const dl = await processAndDownload(page, "sharpening");
if (!dl.ok)
bug({
tool: "sharpening",
setting: "method",
value: "adaptive",
expected: "success",
actual: dl.error ?? "error",
});
expect(dl.ok).toBe(true);
});
test("unsharp-mask method -> runs", async ({ page }) => {
const issues = instrument(page);
await setupTool(page, "sharpening", IMG_200x150);
await page.getByRole("button", { name: "Unsharp Mask" }).first().click();
const dl = await processAndDownload(page, "sharpening");
if (!dl.ok)
bug({
tool: "sharpening",
setting: "method",
value: "unsharp-mask",
expected: "success",
actual: dl.error ?? "error",
});
expect(dl.ok).toBe(true);
});
test("high-pass method -> runs", async ({ page }) => {
const issues = instrument(page);
await setupTool(page, "sharpening", IMG_200x150);
await page.getByRole("button", { name: "High-Pass" }).first().click();
const dl = await processAndDownload(page, "sharpening");
if (!dl.ok)
bug({
tool: "sharpening",
setting: "method",
value: "high-pass",
expected: "success",
actual: dl.error ?? "error",
});
expect(dl.ok).toBe(true);
});
coverage.sharpening = { tested: 3, total: 5 };
});
test.describe("IMAGE: replace-color", () => {
test.setTimeout(TOOL_TIMEOUT);
test("replace red with green -> output changes", async ({ page }) => {
const issues = instrument(page);
await setupTool(page, "replace-color", IMG_200x150);
// Default: sourceColor=#FF0000, targetColor=#00FF00, tolerance=30
const dl = await processAndDownload(page, "replace-color");
if (!dl.ok)
bug({
tool: "replace-color",
setting: "colors",
value: "red->green",
expected: "success",
actual: dl.error ?? "error",
});
expect(dl.ok).toBe(true);
});
test("makeTransparent=true -> output has alpha", async ({ page }) => {
const issues = instrument(page);
await setupTool(page, "replace-color", IMG_200x150);
// Check the "make transparent" checkbox
await page.getByRole("checkbox").first().check();
const dl = await processAndDownload(page, "replace-color");
if (!dl.ok) {
bug({
tool: "replace-color",
setting: "makeTransparent",
value: "true",
expected: "hasAlpha=true",
actual: dl.error ?? "error",
});
return;
}
const info = imageInfo(dl.path);
if (!info.hasAlpha)
bug({
tool: "replace-color",
setting: "makeTransparent",
value: "true",
expected: "hasAlpha=true",
actual: `hasAlpha=${info.hasAlpha}`,
});
});
test("tolerance=0 (exact match) -> runs", async ({ page }) => {
const issues = instrument(page);
await setupTool(page, "replace-color", IMG_200x150);
await setSlider(page, "replace-tolerance", 0);
const dl = await processAndDownload(page, "replace-color");
if (!dl.ok)
bug({
tool: "replace-color",
setting: "tolerance",
value: "0",
expected: "success",
actual: dl.error ?? "error",
});
expect(dl.ok).toBe(true);
});
test("tolerance=255 (max) -> runs", async ({ page }) => {
const issues = instrument(page);
await setupTool(page, "replace-color", IMG_200x150);
await setSlider(page, "replace-tolerance", 255);
const dl = await processAndDownload(page, "replace-color");
if (!dl.ok)
bug({
tool: "replace-color",
setting: "tolerance",
value: "255",
expected: "success",
actual: dl.error ?? "error",
});
expect(dl.ok).toBe(true);
});
coverage["replace-color"] = { tested: 4, total: 4 };
});
// =====================================================================
// VIDEO TOOLS
// =====================================================================
test.describe("VIDEO: trim-video", () => {
test.setTimeout(TOOL_TIMEOUT * 2);
test("startS=0, endS=0.5 -> output duration ~0.5s", async ({ page }) => {
const issues = instrument(page);
await setupTool(page, "trim-video", VID_MP4);
await fillInput(page, "tv-start", 0);
await fillInput(page, "tv-end", 0.5);
const dl = await processAndDownload(page, "trim-video", "long");
if (!dl.ok) {
bug({
tool: "trim-video",
setting: "startS/endS",
value: "0-0.5",
expected: "duration ~0.5s",
actual: dl.error ?? "error",
});
return;
}
const dur = mediaDuration(dl.path);
// Allow tolerance since keyframes may shift
if (dur > 1.0)
bug({
tool: "trim-video",
setting: "startS/endS",
value: "0-0.5",
expected: "dur <= 1.0s",
actual: `dur=${dur.toFixed(2)}s`,
});
expect(dur).toBeLessThan(1.0);
});
coverage["trim-video"] = { tested: 1, total: 3 };
});
test.describe("VIDEO: change-fps", () => {
test.setTimeout(TOOL_TIMEOUT * 2);
test("fps=15 -> output fps ~15", async ({ page }) => {
const issues = instrument(page);
await setupTool(page, "change-fps", VID_MP4);
await fillInput(page, "cfps-fps", 15);
const dl = await processAndDownload(page, "change-fps", "long");
if (!dl.ok) {
bug({
tool: "change-fps",
setting: "fps",
value: "15",
expected: "fps ~15",
actual: dl.error ?? "error",
});
return;
}
const probe = probeMedia(dl.path);
const videoStream = probe.streams?.find((s) => s.codec_type === "video");
// r_frame_rate parsing not exposed; check file at least produced
expect(dl.size).toBeGreaterThan(0);
});
coverage["change-fps"] = { tested: 1, total: 1 };
});
test.describe("VIDEO: video-speed", () => {
test.setTimeout(TOOL_TIMEOUT * 2);
test("factor=2 -> output duration ~half", async ({ page }) => {
const issues = instrument(page);
const baseDur = mediaDuration(VID_MP4);
await setupTool(page, "video-speed", VID_MP4);
await fillInput(page, "vs-factor", 2);
const dl = await processAndDownload(page, "video-speed", "long");
if (!dl.ok) {
bug({
tool: "video-speed",
setting: "factor",
value: "2",
expected: `duration ~${(baseDur / 2).toFixed(2)}s`,
actual: dl.error ?? "error",
});
return;
}
const dur = mediaDuration(dl.path);
// 2x speed -> ~half duration
if (dur > baseDur * 0.8)
bug({
tool: "video-speed",
setting: "factor",
value: "2",
expected: `dur < ${(baseDur * 0.8).toFixed(2)}s`,
actual: `dur=${dur.toFixed(2)}s`,
});
expect(dur).toBeLessThan(baseDur * 0.8);
});
coverage["video-speed"] = { tested: 1, total: 2 };
});
test.describe("VIDEO: resize-video", () => {
test.setTimeout(TOOL_TIMEOUT * 2);
test("preset=360p -> output height ~360", async ({ page }) => {
const issues = instrument(page);
await setupTool(page, "resize-video", VID_MP4);
await selectOption(page, "rv-preset", "360p");
const dl = await processAndDownload(page, "resize-video", "long");
if (!dl.ok) {
bug({
tool: "resize-video",
setting: "preset",
value: "360p",
expected: "height ~360",
actual: dl.error ?? "error",
});
return;
}
const probe = probeMedia(dl.path);
const videoStream = probe.streams?.find((s) => s.codec_type === "video");
// 360p sets height to 360, width auto. Source is 64x64 so may upscale to 360x360
expect(videoStream?.height).toBeDefined();
});
test("custom width=32, height=32 -> output 32x32", async ({ page }) => {
const issues = instrument(page);
await setupTool(page, "resize-video", VID_MP4);
await selectOption(page, "rv-preset", "custom");
await fillInput(page, "rv-width", 32);
await fillInput(page, "rv-height", 32);
const dl = await processAndDownload(page, "resize-video", "long");
if (!dl.ok) {
bug({
tool: "resize-video",
setting: "dims",
value: "32x32",
expected: "32x32",
actual: dl.error ?? "error",
});
return;
}
const probe = probeMedia(dl.path);
const vs = probe.streams?.find((s) => s.codec_type === "video");
if (vs && (vs.width !== 32 || vs.height !== 32))
bug({
tool: "resize-video",
setting: "dims",
value: "32x32",
expected: "32x32",
actual: `${vs.width}x${vs.height}`,
});
});
coverage["resize-video"] = { tested: 2, total: 3 };
});
test.describe("VIDEO: rotate-video", () => {
test.setTimeout(TOOL_TIMEOUT * 2);
test("cw90 -> dims swap (64x64 stays 64x64 for square)", async ({ page }) => {
const issues = instrument(page);
await setupTool(page, "rotate-video", VID_MP4);
await selectOption(page, "rtv-transform", "cw90");
const dl = await processAndDownload(page, "rotate-video", "long");
if (!dl.ok) {
bug({
tool: "rotate-video",
setting: "transform",
value: "cw90",
expected: "success",
actual: dl.error ?? "error",
});
return;
}
const probe = probeMedia(dl.path);
const vs = probe.streams?.find((s) => s.codec_type === "video");
// Square video: dims stay same. Just verify it ran
expect(dl.size).toBeGreaterThan(0);
});
test("180 -> runs successfully", async ({ page }) => {
const issues = instrument(page);
await setupTool(page, "rotate-video", VID_MP4);
await selectOption(page, "rtv-transform", "180");
const dl = await processAndDownload(page, "rotate-video", "long");
if (!dl.ok)
bug({
tool: "rotate-video",
setting: "transform",
value: "180",
expected: "success",
actual: dl.error ?? "error",
});
expect(dl.ok).toBe(true);
});
test("hflip -> runs successfully", async ({ page }) => {
const issues = instrument(page);
await setupTool(page, "rotate-video", VID_MP4);
await selectOption(page, "rtv-transform", "hflip");
const dl = await processAndDownload(page, "rotate-video", "long");
if (!dl.ok)
bug({
tool: "rotate-video",
setting: "transform",
value: "hflip",
expected: "success",
actual: dl.error ?? "error",
});
expect(dl.ok).toBe(true);
});
test("vflip -> runs successfully", async ({ page }) => {
const issues = instrument(page);
await setupTool(page, "rotate-video", VID_MP4);
await selectOption(page, "rtv-transform", "vflip");
const dl = await processAndDownload(page, "rotate-video", "long");
if (!dl.ok)
bug({
tool: "rotate-video",
setting: "transform",
value: "vflip",
expected: "success",
actual: dl.error ?? "error",
});
expect(dl.ok).toBe(true);
});
coverage["rotate-video"] = { tested: 4, total: 5 };
});
test.describe("VIDEO: crop-video", () => {
test.setTimeout(TOOL_TIMEOUT * 2);
test("crop to 32x32 at 0,0 -> output 32x32", async ({ page }) => {
const issues = instrument(page);
await setupTool(page, "crop-video", VID_MP4);
await fillInput(page, "crv-width", 32);
await fillInput(page, "crv-height", 32);
await fillInput(page, "crv-x", 0);
await fillInput(page, "crv-y", 0);
const dl = await processAndDownload(page, "crop-video", "long");
if (!dl.ok) {
bug({
tool: "crop-video",
setting: "region",
value: "32x32@0,0",
expected: "32x32",
actual: dl.error ?? "error",
});
return;
}
const probe = probeMedia(dl.path);
const vs = probe.streams?.find((s) => s.codec_type === "video");
if (vs && (vs.width !== 32 || vs.height !== 32))
bug({
tool: "crop-video",
setting: "region",
value: "32x32@0,0",
expected: "32x32",
actual: `${vs.width}x${vs.height}`,
});
expect(vs?.width).toBe(32);
expect(vs?.height).toBe(32);
});
coverage["crop-video"] = { tested: 1, total: 1 };
});
// =====================================================================
// AUDIO TOOLS
// =====================================================================
test.describe("AUDIO: trim-audio", () => {
test.setTimeout(TOOL_TIMEOUT * 2);
test("startS=0, endS=0.5 -> output duration ~0.5s", async ({ page }) => {
const issues = instrument(page);
await setupTool(page, "trim-audio", AUD_MP3);
await fillInput(page, "ta-start", 0);
await fillInput(page, "ta-end", 0.5);
const dl = await processAndDownload(page, "trim-audio", "long");
if (!dl.ok) {
bug({
tool: "trim-audio",
setting: "startS/endS",
value: "0-0.5",
expected: "dur ~0.5s",
actual: dl.error ?? "error",
});
return;
}
const dur = mediaDuration(dl.path);
if (dur > 1.0)
bug({
tool: "trim-audio",
setting: "startS/endS",
value: "0-0.5",
expected: "dur <= 1.0s",
actual: `dur=${dur.toFixed(2)}s`,
});
expect(dur).toBeLessThan(1.0);
});
coverage["trim-audio"] = { tested: 1, total: 1 };
});
test.describe("AUDIO: audio-speed", () => {
test.setTimeout(TOOL_TIMEOUT * 2);
test("factor=2 -> output duration ~half", async ({ page }) => {
const issues = instrument(page);
const baseDur = mediaDuration(AUD_MP3);
await setupTool(page, "audio-speed", AUD_MP3);
await fillInput(page, "as-factor", 2);
const dl = await processAndDownload(page, "audio-speed", "long");
if (!dl.ok) {
bug({
tool: "audio-speed",
setting: "factor",
value: "2",
expected: `dur ~${(baseDur / 2).toFixed(2)}s`,
actual: dl.error ?? "error",
});
return;
}
const dur = mediaDuration(dl.path);
if (dur > baseDur * 0.8)
bug({
tool: "audio-speed",
setting: "factor",
value: "2",
expected: `dur < ${(baseDur * 0.8).toFixed(2)}s`,
actual: `dur=${dur.toFixed(2)}s`,
});
expect(dur).toBeLessThan(baseDur * 0.8);
});
test("factor=0.5 -> output duration ~double", async ({ page }) => {
const issues = instrument(page);
const baseDur = mediaDuration(AUD_MP3);
await setupTool(page, "audio-speed", AUD_MP3);
await fillInput(page, "as-factor", 0.5);
const dl = await processAndDownload(page, "audio-speed", "long");
if (!dl.ok) {
bug({
tool: "audio-speed",
setting: "factor",
value: "0.5",
expected: `dur ~${(baseDur * 2).toFixed(2)}s`,
actual: dl.error ?? "error",
});
return;
}
const dur = mediaDuration(dl.path);
if (dur < baseDur * 1.5)
bug({
tool: "audio-speed",
setting: "factor",
value: "0.5",
expected: `dur > ${(baseDur * 1.5).toFixed(2)}s`,
actual: `dur=${dur.toFixed(2)}s`,
});
expect(dur).toBeGreaterThan(baseDur * 1.5);
});
coverage["audio-speed"] = { tested: 2, total: 2 };
});
test.describe("AUDIO: volume-adjust", () => {
test.setTimeout(TOOL_TIMEOUT * 2);
test("gainDb=10 -> runs successfully", async ({ page }) => {
const issues = instrument(page);
await setupTool(page, "volume-adjust", AUD_MP3);
await fillInput(page, "va-gain", 10);
const dl = await processAndDownload(page, "volume-adjust", "long");
if (!dl.ok)
bug({
tool: "volume-adjust",
setting: "gainDb",
value: "10",
expected: "success",
actual: dl.error ?? "error",
});
expect(dl.ok).toBe(true);
});
test("gainDb=-10 -> runs successfully", async ({ page }) => {
const issues = instrument(page);
await setupTool(page, "volume-adjust", AUD_MP3);
await fillInput(page, "va-gain", -10);
const dl = await processAndDownload(page, "volume-adjust", "long");
if (!dl.ok)
bug({
tool: "volume-adjust",
setting: "gainDb",
value: "-10",
expected: "success",
actual: dl.error ?? "error",
});
expect(dl.ok).toBe(true);
});
coverage["volume-adjust"] = { tested: 2, total: 2 };
});
test.describe("AUDIO: fade-audio", () => {
test.setTimeout(TOOL_TIMEOUT * 2);
test("fadeInS=0.5, fadeOutS=0.5 -> runs, duration preserved", async ({ page }) => {
const issues = instrument(page);
const baseDur = mediaDuration(AUD_MP3);
await setupTool(page, "fade-audio", AUD_MP3);
await fillInput(page, "fa-fadein", 0.5);
await fillInput(page, "fa-fadeout", 0.5);
const dl = await processAndDownload(page, "fade-audio", "long");
if (!dl.ok) {
bug({
tool: "fade-audio",
setting: "fadeInS/fadeOutS",
value: "0.5/0.5",
expected: "success, duration preserved",
actual: dl.error ?? "error",
});
return;
}
const dur = mediaDuration(dl.path);
// Duration should be approximately the same
if (Math.abs(dur - baseDur) > 0.5)
warn({
tool: "fade-audio",
setting: "fadeInS/fadeOutS",
value: "0.5/0.5",
expected: `dur ~${baseDur.toFixed(2)}s`,
actual: `dur=${dur.toFixed(2)}s`,
});
expect(dl.size).toBeGreaterThan(0);
});
coverage["fade-audio"] = { tested: 1, total: 2 };
});
test.describe("AUDIO: normalize-audio", () => {
test.setTimeout(TOOL_TIMEOUT * 2);
test("normalize -> runs successfully", async ({ page }) => {
const issues = instrument(page);
await setupTool(page, "normalize-audio", AUD_MP3);
const dl = await processAndDownload(page, "normalize-audio", "long");
if (!dl.ok)
bug({
tool: "normalize-audio",
setting: "default",
value: "default",
expected: "success",
actual: dl.error ?? "error",
});
expect(dl.ok).toBe(true);
});
coverage["normalize-audio"] = { tested: 1, total: 1 };
});
test.describe("AUDIO: audio-channels", () => {
test.setTimeout(TOOL_TIMEOUT * 2);
test("stereo-to-mono -> output has 1 channel", async ({ page }) => {
const issues = instrument(page);
await setupTool(page, "audio-channels", AUD_STEREO);
await selectOption(page, "ac-mode", "stereo-to-mono");
const dl = await processAndDownload(page, "audio-channels", "long");
if (!dl.ok) {
bug({
tool: "audio-channels",
setting: "mode",
value: "stereo-to-mono",
expected: "1 channel",
actual: dl.error ?? "error",
});
return;
}
const probe = probeMedia(dl.path);
const audioStream = probe.streams?.find((s) => s.codec_type === "audio");
if (audioStream && audioStream.channels !== 1)
bug({
tool: "audio-channels",
setting: "mode",
value: "stereo-to-mono",
expected: "channels=1",
actual: `channels=${audioStream.channels}`,
});
expect(audioStream?.channels).toBe(1);
});
test("mono-to-stereo with mono source -> output has 2 channels", async ({ page }) => {
const issues = instrument(page);
await setupTool(page, "audio-channels", AUD_MP3);
await selectOption(page, "ac-mode", "mono-to-stereo");
const dl = await processAndDownload(page, "audio-channels", "long");
if (!dl.ok) {
bug({
tool: "audio-channels",
setting: "mode",
value: "mono-to-stereo",
expected: "2 channels",
actual: dl.error ?? "error",
});
return;
}
const probe = probeMedia(dl.path);
const audioStream = probe.streams?.find((s) => s.codec_type === "audio");
if (audioStream && audioStream.channels !== 2)
bug({
tool: "audio-channels",
setting: "mode",
value: "mono-to-stereo",
expected: "channels=2",
actual: `channels=${audioStream.channels}`,
});
expect(audioStream?.channels).toBe(2);
});
test("swap channels on stereo -> runs successfully", async ({ page }) => {
const issues = instrument(page);
await setupTool(page, "audio-channels", AUD_STEREO);
await selectOption(page, "ac-mode", "swap");
const dl = await processAndDownload(page, "audio-channels", "long");
if (!dl.ok)
bug({
tool: "audio-channels",
setting: "mode",
value: "swap",
expected: "success",
actual: dl.error ?? "error",
});
expect(dl.ok).toBe(true);
});
coverage["audio-channels"] = { tested: 3, total: 3 };
});
// =====================================================================
// DOCUMENT TOOLS
// =====================================================================
test.describe("DOCUMENT: rotate-pdf", () => {
test.setTimeout(TOOL_TIMEOUT * 2);
test("angle=90 -> output is valid PDF", async ({ page }) => {
const issues = instrument(page);
await setupTool(page, "rotate-pdf", PDF_3PAGE);
await selectOption(page, "rpdf-angle", "90");
const dl = await processAndDownload(page, "rotate-pdf", "long");
if (!dl.ok) {
bug({
tool: "rotate-pdf",
setting: "angle",
value: "90",
expected: "valid PDF",
actual: dl.error ?? "error",
});
return;
}
if (!magicMatches(dl.buf, "pdf"))
bug({
tool: "rotate-pdf",
setting: "angle",
value: "90",
expected: "PDF magic bytes",
actual: "not a PDF",
});
expect(magicMatches(dl.buf, "pdf")).toBe(true);
});
test("angle=180 -> output is valid PDF", async ({ page }) => {
const issues = instrument(page);
await setupTool(page, "rotate-pdf", PDF_3PAGE);
await selectOption(page, "rpdf-angle", "180");
const dl = await processAndDownload(page, "rotate-pdf", "long");
if (!dl.ok)
bug({
tool: "rotate-pdf",
setting: "angle",
value: "180",
expected: "success",
actual: dl.error ?? "error",
});
expect(dl.ok).toBe(true);
});
test("angle=270 -> output is valid PDF", async ({ page }) => {
const issues = instrument(page);
await setupTool(page, "rotate-pdf", PDF_3PAGE);
await selectOption(page, "rpdf-angle", "270");
const dl = await processAndDownload(page, "rotate-pdf", "long");
if (!dl.ok)
bug({
tool: "rotate-pdf",
setting: "angle",
value: "270",
expected: "success",
actual: dl.error ?? "error",
});
expect(dl.ok).toBe(true);
});
coverage["rotate-pdf"] = { tested: 3, total: 3 };
});
test.describe("DOCUMENT: split-pdf", () => {
test.setTimeout(TOOL_TIMEOUT * 2);
test("range='1-2' on 3-page PDF -> output has 2 pages (or ZIP of 2-page PDF)", async ({
page,
}) => {
const issues = instrument(page);
await setupTool(page, "split-pdf", PDF_3PAGE);
// Default mode is range, range="1-3,5"
await fillInput(page, "sp-range", "1-2");
const dl = await processAndDownload(page, "split-pdf", "long");
if (!dl.ok) {
bug({
tool: "split-pdf",
setting: "range",
value: "1-2",
expected: "success",
actual: dl.error ?? "error",
});
return;
}
// Result could be a PDF or ZIP depending on implementation
const isPdf = magicMatches(dl.buf, "pdf");
const isZip = magicMatches(dl.buf, "zip");
if (!isPdf && !isZip)
bug({
tool: "split-pdf",
setting: "range",
value: "1-2",
expected: "PDF or ZIP output",
actual: "unknown format",
});
expect(isPdf || isZip).toBe(true);
});
coverage["split-pdf"] = { tested: 1, total: 2 };
});
test.describe("DOCUMENT: extract-pages", () => {
test.setTimeout(TOOL_TIMEOUT * 2);
test("range='1' on 3-page PDF -> output has 1 page", async ({ page }) => {
const issues = instrument(page);
await setupTool(page, "extract-pages", PDF_3PAGE);
await fillInput(page, "ep-range", "1");
const dl = await processAndDownload(page, "extract-pages", "long");
if (!dl.ok) {
bug({
tool: "extract-pages",
setting: "range",
value: "1",
expected: "1-page PDF",
actual: dl.error ?? "error",
});
return;
}
if (!magicMatches(dl.buf, "pdf"))
bug({
tool: "extract-pages",
setting: "range",
value: "1",
expected: "PDF output",
actual: "not a PDF",
});
expect(magicMatches(dl.buf, "pdf")).toBe(true);
});
test("range='2-3' on 3-page PDF -> output has 2 pages", async ({ page }) => {
const issues = instrument(page);
await setupTool(page, "extract-pages", PDF_3PAGE);
await fillInput(page, "ep-range", "2-3");
const dl = await processAndDownload(page, "extract-pages", "long");
if (!dl.ok) {
bug({
tool: "extract-pages",
setting: "range",
value: "2-3",
expected: "2-page PDF",
actual: dl.error ?? "error",
});
return;
}
expect(magicMatches(dl.buf, "pdf")).toBe(true);
});
coverage["extract-pages"] = { tested: 2, total: 2 };
});
test.describe("DOCUMENT: compress-pdf", () => {
test.setTimeout(TOOL_TIMEOUT * 2);
// compress-pdf uses the shared CompressControls (quality / target-size modes),
// not screen/ebook/printer ghostscript presets. These drive the actual UI.
test("quality mode (q=20) -> output is valid PDF", async ({ page }) => {
const issues = instrument(page);
await setupTool(page, "compress-pdf", PDF_3PAGE);
await page
.getByRole("button", { name: /quality/i })
.first()
.click();
await setSlider(page, "compress-quality", 20);
const dl = await processAndDownload(page, "compress-pdf", "long");
if (!dl.ok) {
bug({
tool: "compress-pdf",
setting: "quality",
value: "20",
expected: "valid PDF",
actual: dl.error ?? "error",
});
return;
}
expect(magicMatches(dl.buf, "pdf")).toBe(true);
});
test("quality mode (q=80) -> output is valid PDF", async ({ page }) => {
const issues = instrument(page);
await setupTool(page, "compress-pdf", PDF_3PAGE);
await page
.getByRole("button", { name: /quality/i })
.first()
.click();
await setSlider(page, "compress-quality", 80);
const dl = await processAndDownload(page, "compress-pdf", "long");
if (!dl.ok)
bug({
tool: "compress-pdf",
setting: "quality",
value: "80",
expected: "success",
actual: dl.error ?? "error",
});
expect(dl.ok).toBe(true);
});
test("target-size mode -> output is valid PDF", async ({ page }) => {
const issues = instrument(page);
await setupTool(page, "compress-pdf", PDF_3PAGE);
await fillInput(page, "compress-target-size", 100);
const dl = await processAndDownload(page, "compress-pdf", "long");
if (!dl.ok)
bug({
tool: "compress-pdf",
setting: "targetSize",
value: "100KB",
expected: "success",
actual: dl.error ?? "error",
});
expect(dl.ok).toBe(true);
});
coverage["compress-pdf"] = { tested: 3, total: 3 };
});
test.describe("DOCUMENT: nup-pdf", () => {
test.setTimeout(TOOL_TIMEOUT * 2);
test("perSheet=2 on 3-page PDF -> output has fewer pages", async ({ page }) => {
const issues = instrument(page);
await setupTool(page, "nup-pdf", PDF_3PAGE);
await selectOption(page, "nup-per-sheet", "2");
const dl = await processAndDownload(page, "nup-pdf", "long");
if (!dl.ok) {
bug({
tool: "nup-pdf",
setting: "perSheet",
value: "2",
expected: "valid PDF with fewer pages",
actual: dl.error ?? "error",
});
return;
}
expect(magicMatches(dl.buf, "pdf")).toBe(true);
// 3 pages with 2-up = 2 output pages (ceil(3/2))
expect(dl.size).toBeGreaterThan(0);
});
test("perSheet=4 on 3-page PDF -> output has 1 page", async ({ page }) => {
const issues = instrument(page);
await setupTool(page, "nup-pdf", PDF_3PAGE);
await selectOption(page, "nup-per-sheet", "4");
const dl = await processAndDownload(page, "nup-pdf", "long");
if (!dl.ok)
bug({
tool: "nup-pdf",
setting: "perSheet",
value: "4",
expected: "success",
actual: dl.error ?? "error",
});
expect(dl.ok).toBe(true);
});
coverage["nup-pdf"] = { tested: 2, total: 2 };
});
test.describe("DOCUMENT: grayscale-pdf", () => {
test.setTimeout(TOOL_TIMEOUT * 2);
test("grayscale -> output is valid PDF", async ({ page }) => {
const issues = instrument(page);
await setupTool(page, "grayscale-pdf", PDF_3PAGE);
const dl = await processAndDownload(page, "grayscale-pdf", "long");
if (!dl.ok) {
bug({
tool: "grayscale-pdf",
setting: "default",
value: "default",
expected: "valid grayscale PDF",
actual: dl.error ?? "error",
});
return;
}
expect(magicMatches(dl.buf, "pdf")).toBe(true);
});
coverage["grayscale-pdf"] = { tested: 1, total: 1 };
});