mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
Empty the hardcoded page-range default in remove/split/extract PDF tools (remove-pages defaulted to "2,4-6", out of range for any PDF under 6 pages) and disable submit until a range is entered. Reject password-protected PDFs up front for PDF-only tools with guidance to unlock first, instead of failing cryptically in the qpdf worker. Adds integration + e2e coverage.
2161 lines
65 KiB
TypeScript
2161 lines
65 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("image", "valid", "test-200x150.png"); // 200x150 PNG
|
|
const IMG_JPG = fixture("image", "valid", "sample-photo.jpg");
|
|
const VID_MP4 = fixture("video", "formats", "tiny.mp4"); // 64x64, 1s, 8fps
|
|
const AUD_MP3 = fixture("audio", "formats", "tiny.mp3"); // mono, ~1s
|
|
const AUD_STEREO = fixture("audio", "formats", "tone-stereo.wav"); // stereo, 1s
|
|
const PDF_3PAGE = fixture("document", "valid", "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();
|
|
// The current fixture has no pixels near pure red at the default tolerance.
|
|
// Use the full tolerance range so this assertion exercises transparent output.
|
|
await setSlider(page, "replace-tolerance", 255);
|
|
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}`,
|
|
});
|
|
expect(info.hasAlpha).toBe(true);
|
|
});
|
|
|
|
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; the range box starts empty (placeholder "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 };
|
|
});
|