test: reorganize fixture files into modality-first layout (phase 6b)
Move all fixture files from flat/mixed dirs (content/, media/, documents/,
formats/, hostile/, root loose) into the modality-first hierarchy:
image/{valid,formats,edge,hostile}, video/{valid,formats,hostile},
audio/{valid,formats,hostile}, document/{valid,formats,edge,hostile},
data/valid/, security/. Update index.ts paths, fixtureDir aliases,
all literal refs in 17 e2e/qa/script files, manifest.json, and the
three generator scripts. 163 files moved, 0 dropped, 100 new tests
from expanded document scan.
@@ -3,7 +3,7 @@
|
|||||||
// make-tiny-pptx.mjs
|
// make-tiny-pptx.mjs
|
||||||
//
|
//
|
||||||
// Builds a minimal ECMA-376 (OOXML) presentation skeleton and writes it to
|
// Builds a minimal ECMA-376 (OOXML) presentation skeleton and writes it to
|
||||||
// tests/fixtures/documents/tiny.pptx using adm-zip (root devDep).
|
// tests/fixtures/document/formats/tiny.pptx using adm-zip (root devDep).
|
||||||
//
|
//
|
||||||
// ACCEPTANCE (2026-06-12, snapotter/snapotter:latest + libreoffice-impress):
|
// ACCEPTANCE (2026-06-12, snapotter/snapotter:latest + libreoffice-impress):
|
||||||
// soffice --headless --norestore --nolockcheck --nodefault \
|
// soffice --headless --norestore --nolockcheck --nodefault \
|
||||||
@@ -18,7 +18,7 @@ import { fileURLToPath } from "node:url";
|
|||||||
import AdmZip from "adm-zip";
|
import AdmZip from "adm-zip";
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
const outPath = join(__dirname, "..", "..", "tests", "fixtures", "documents", "tiny.pptx");
|
const outPath = join(__dirname, "..", "..", "tests", "fixtures", "document", "formats", "tiny.pptx");
|
||||||
|
|
||||||
const zip = new AdmZip();
|
const zip = new AdmZip();
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
/**
|
/**
|
||||||
* Generates the hostile-input fixtures in tests/fixtures/hostile/.
|
* Generates the hostile-input fixtures in tests/fixtures/image/hostile/.
|
||||||
*
|
*
|
||||||
* Deterministic on purpose: re-running produces byte-identical files so the
|
* Deterministic on purpose: re-running produces byte-identical files so the
|
||||||
* committed fixtures never churn. No dependencies (the bomb PNG is built by
|
* committed fixtures never churn. No dependencies (the bomb PNG is built by
|
||||||
@@ -14,11 +14,11 @@ import { fileURLToPath } from "node:url";
|
|||||||
import zlib from "node:zlib";
|
import zlib from "node:zlib";
|
||||||
|
|
||||||
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||||
const outDir = join(root, "tests", "fixtures", "hostile");
|
const outDir = join(root, "tests", "fixtures", "image", "hostile");
|
||||||
mkdirSync(outDir, { recursive: true });
|
mkdirSync(outDir, { recursive: true });
|
||||||
|
|
||||||
// --- 1. truncated.jpg: a real JPEG cut off at 40% ---------------------------
|
// --- 1. truncated.jpg: a real JPEG cut off at 40% ---------------------------
|
||||||
const realJpeg = readFileSync(join(root, "tests", "fixtures", "sample-photo.jpg"));
|
const realJpeg = readFileSync(join(root, "tests", "fixtures", "image", "valid", "sample-photo.jpg"));
|
||||||
writeFileSync(
|
writeFileSync(
|
||||||
join(outDir, "truncated.jpg"),
|
join(outDir, "truncated.jpg"),
|
||||||
realJpeg.subarray(0, Math.floor(realJpeg.length * 0.4)),
|
realJpeg.subarray(0, Math.floor(realJpeg.length * 0.4)),
|
||||||
@@ -38,7 +38,7 @@ for (let i = 0; i < garbage.length; i++) {
|
|||||||
writeFileSync(join(outDir, "garbage.jpg"), garbage);
|
writeFileSync(join(outDir, "garbage.jpg"), garbage);
|
||||||
|
|
||||||
// --- 4. png-bytes.jpg: valid PNG content with a lying .jpg extension --------
|
// --- 4. png-bytes.jpg: valid PNG content with a lying .jpg extension --------
|
||||||
const realPng = readFileSync(join(root, "tests", "fixtures", "test-200x150.png"));
|
const realPng = readFileSync(join(root, "tests", "fixtures", "image", "valid", "test-200x150.png"));
|
||||||
writeFileSync(join(outDir, "png-bytes.jpg"), realPng);
|
writeFileSync(join(outDir, "png-bytes.jpg"), realPng);
|
||||||
|
|
||||||
// --- 5. bomb-50000x50000.png: tiny file whose header claims 2,500 megapixels
|
// --- 5. bomb-50000x50000.png: tiny file whose header claims 2,500 megapixels
|
||||||
|
|||||||
@@ -13,9 +13,11 @@ import { fileURLToPath } from "node:url";
|
|||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
const root = join(__dirname, "..");
|
const root = join(__dirname, "..");
|
||||||
const mediaDir = join(root, "tests/fixtures/media");
|
const videoDir = join(root, "tests/fixtures/video/formats");
|
||||||
const docsDir = join(root, "tests/fixtures/documents");
|
const audioDir = join(root, "tests/fixtures/audio/formats");
|
||||||
mkdirSync(mediaDir, { recursive: true });
|
const docsDir = join(root, "tests/fixtures/document/formats");
|
||||||
|
mkdirSync(videoDir, { recursive: true });
|
||||||
|
mkdirSync(audioDir, { recursive: true });
|
||||||
mkdirSync(docsDir, { recursive: true });
|
mkdirSync(docsDir, { recursive: true });
|
||||||
|
|
||||||
const ffmpeg = process.env.FFMPEG_PATH || "ffmpeg";
|
const ffmpeg = process.env.FFMPEG_PATH || "ffmpeg";
|
||||||
@@ -58,7 +60,7 @@ run([
|
|||||||
"-b:a",
|
"-b:a",
|
||||||
"32k",
|
"32k",
|
||||||
"-shortest",
|
"-shortest",
|
||||||
join(mediaDir, "tiny.mp4"),
|
join(videoDir, "tiny.mp4"),
|
||||||
]);
|
]);
|
||||||
// 1s sine mp3
|
// 1s sine mp3
|
||||||
run([
|
run([
|
||||||
@@ -70,7 +72,7 @@ run([
|
|||||||
"libmp3lame",
|
"libmp3lame",
|
||||||
"-b:a",
|
"-b:a",
|
||||||
"32k",
|
"32k",
|
||||||
join(mediaDir, "tiny.mp3"),
|
join(audioDir, "tiny.mp3"),
|
||||||
]);
|
]);
|
||||||
// 1s sine wav
|
// 1s sine wav
|
||||||
run([
|
run([
|
||||||
@@ -82,7 +84,7 @@ run([
|
|||||||
"pcm_s16le",
|
"pcm_s16le",
|
||||||
"-ar",
|
"-ar",
|
||||||
"8000",
|
"8000",
|
||||||
join(mediaDir, "tiny.wav"),
|
join(audioDir, "tiny.wav"),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Minimal OOXML/EPUB containers via archiver (resolved from apps/api).
|
// Minimal OOXML/EPUB containers via archiver (resolved from apps/api).
|
||||||
@@ -154,8 +156,8 @@ await writeZip(
|
|||||||
// Encrypted PDF fixture derived from test-3page.pdf via qpdf (AES-256).
|
// Encrypted PDF fixture derived from test-3page.pdf via qpdf (AES-256).
|
||||||
const qpdf = whichBin("QPDF_PATH", "qpdf");
|
const qpdf = whichBin("QPDF_PATH", "qpdf");
|
||||||
if (qpdf) {
|
if (qpdf) {
|
||||||
const srcPdf = join(root, "tests/fixtures/test-3page.pdf");
|
const srcPdf = join(root, "tests/fixtures/document/valid/test-3page.pdf");
|
||||||
const encPdf = join(docsDir, "encrypted.pdf");
|
const encPdf = join(root, "tests/fixtures/document/valid/encrypted.pdf");
|
||||||
const qres = spawnSync(qpdf, [srcPdf, "--encrypt", "test123", "owner123", "256", "--", encPdf], {
|
const qres = spawnSync(qpdf, [srcPdf, "--encrypt", "test123", "owner123", "256", "--", encPdf], {
|
||||||
stdio: "inherit",
|
stdio: "inherit",
|
||||||
});
|
});
|
||||||
@@ -192,4 +194,4 @@ A paragraph with **bold** and a list:
|
|||||||
`,
|
`,
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log("Fixtures written to tests/fixtures/media and tests/fixtures/documents");
|
console.log("Fixtures written to tests/fixtures/{video,audio}/formats and tests/fixtures/document/formats");
|
||||||
|
|||||||
@@ -318,7 +318,7 @@ test.describe("Editor Menu Bar", () => {
|
|||||||
const fileChooserPromise = page.waitForEvent("filechooser");
|
const fileChooserPromise = page.waitForEvent("filechooser");
|
||||||
await page.click('[data-testid="menu-item-open"]');
|
await page.click('[data-testid="menu-item-open"]');
|
||||||
const chooser = await fileChooserPromise;
|
const chooser = await fileChooserPromise;
|
||||||
await chooser.setFiles("tests/fixtures/test-200x150.png");
|
await chooser.setFiles("tests/fixtures/image/valid/test-200x150.png");
|
||||||
await page.waitForTimeout(1000);
|
await page.waitForTimeout(1000);
|
||||||
await page.click('[data-testid="menu-select"]');
|
await page.click('[data-testid="menu-select"]');
|
||||||
await page.click('[data-testid="menu-item-all"]');
|
await page.click('[data-testid="menu-item-all"]');
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export async function drawOnCanvas(
|
|||||||
export async function loadTestImage(page: Page): Promise<void> {
|
export async function loadTestImage(page: Page): Promise<void> {
|
||||||
await page.route("**/test-fixture.png", (route) =>
|
await page.route("**/test-fixture.png", (route) =>
|
||||||
route.fulfill({
|
route.fulfill({
|
||||||
path: "tests/fixtures/test-200x150.png",
|
path: "tests/fixtures/image/valid/test-200x150.png",
|
||||||
contentType: "image/png",
|
contentType: "image/png",
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { expect, test, waitForProcessing } from "./helpers";
|
import { expect, test, waitForProcessing } from "./helpers";
|
||||||
|
|
||||||
const PDF_FIXTURE = path.join(process.cwd(), "tests", "fixtures", "test-3page.pdf");
|
const PDF_FIXTURE = path.join(
|
||||||
|
process.cwd(),
|
||||||
|
"tests",
|
||||||
|
"fixtures",
|
||||||
|
"document",
|
||||||
|
"valid",
|
||||||
|
"test-3page.pdf",
|
||||||
|
);
|
||||||
|
|
||||||
test.describe("Document display mode (rotate-pdf)", () => {
|
test.describe("Document display mode (rotate-pdf)", () => {
|
||||||
test("uploads a PDF, rotates it, and shows the document canvas with processed result", async ({
|
test("uploads a PDF, rotates it, and shows the document canvas with processed result", async ({
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ import path from "node:path";
|
|||||||
import { expect, test, uploadTestImage, waitForProcessing } from "./helpers";
|
import { expect, test, uploadTestImage, waitForProcessing } from "./helpers";
|
||||||
|
|
||||||
const fixtureFormat = (name: string) =>
|
const fixtureFormat = (name: string) =>
|
||||||
path.join(process.cwd(), "tests", "fixtures", "formats", name);
|
path.join(process.cwd(), "tests", "fixtures", "image", "formats", name);
|
||||||
const fixtureRoot = (name: string) => path.join(process.cwd(), "tests", "fixtures", name);
|
const fixtureImage = (name: string) =>
|
||||||
|
path.join(process.cwd(), "tests", "fixtures", "image", "valid", name);
|
||||||
|
|
||||||
async function uploadFixture(page: import("@playwright/test").Page, filePath: string) {
|
async function uploadFixture(page: import("@playwright/test").Page, filePath: string) {
|
||||||
const fileChooserPromise = page.waitForEvent("filechooser");
|
const fileChooserPromise = page.waitForEvent("filechooser");
|
||||||
@@ -39,7 +40,7 @@ test.describe("Format upload and resize processing", () => {
|
|||||||
}
|
}
|
||||||
test("HEIC uploads and resizes", async ({ loggedInPage: page }) => {
|
test("HEIC uploads and resizes", async ({ loggedInPage: page }) => {
|
||||||
await page.goto("/resize");
|
await page.goto("/resize");
|
||||||
await uploadFixture(page, fixtureRoot("test-200x150.heic"));
|
await uploadFixture(page, fixtureImage("test-200x150.heic"));
|
||||||
await page.locator("input[placeholder='Auto']").first().fill("25");
|
await page.locator("input[placeholder='Auto']").first().fill("25");
|
||||||
await page.getByRole("button", { name: "Resize" }).click();
|
await page.getByRole("button", { name: "Resize" }).click();
|
||||||
await waitForProcessing(page);
|
await waitForProcessing(page);
|
||||||
@@ -108,7 +109,7 @@ test.describe("HEIC-to-JPG conversion", () => {
|
|||||||
test.describe.configure({ timeout: 60_000 });
|
test.describe.configure({ timeout: 60_000 });
|
||||||
test("uploads HEIC, converts to JPG", async ({ loggedInPage: page }) => {
|
test("uploads HEIC, converts to JPG", async ({ loggedInPage: page }) => {
|
||||||
await page.goto("/convert");
|
await page.goto("/convert");
|
||||||
await uploadFixture(page, fixtureRoot("test-200x150.heic"));
|
await uploadFixture(page, fixtureImage("test-200x150.heic"));
|
||||||
await expect(page.getByText(/heic/i).first()).toBeVisible({ timeout: 10_000 });
|
await expect(page.getByText(/heic/i).first()).toBeVisible({ timeout: 10_000 });
|
||||||
await page.selectOption("#convert-target-format", "jpg");
|
await page.selectOption("#convert-target-format", "jpg");
|
||||||
await page.getByRole("button", { name: /convert/i }).click();
|
await page.getByRole("button", { name: /convert/i }).click();
|
||||||
|
|||||||
@@ -256,7 +256,14 @@ test.describe("GUI Essential Tools", () => {
|
|||||||
}) => {
|
}) => {
|
||||||
await page.goto("/crop");
|
await page.goto("/crop");
|
||||||
|
|
||||||
const portraitPath = path.join(process.cwd(), "tests", "fixtures", "test-portrait-tall.png");
|
const portraitPath = path.join(
|
||||||
|
process.cwd(),
|
||||||
|
"tests",
|
||||||
|
"fixtures",
|
||||||
|
"image",
|
||||||
|
"edge",
|
||||||
|
"test-portrait-tall.png",
|
||||||
|
);
|
||||||
const fileChooserPromise = page.waitForEvent("filechooser");
|
const fileChooserPromise = page.waitForEvent("filechooser");
|
||||||
await page.locator("[class*='border-dashed']").first().click();
|
await page.locator("[class*='border-dashed']").first().click();
|
||||||
const fileChooser = await fileChooserPromise;
|
const fileChooser = await fileChooserPromise;
|
||||||
|
|||||||
@@ -197,7 +197,14 @@ test.describe("GUI Expanded Tool Coverage", () => {
|
|||||||
await page.goto("/compare");
|
await page.goto("/compare");
|
||||||
|
|
||||||
// Set second image via file input
|
// Set second image via file input
|
||||||
const testFixture = path.join(process.cwd(), "tests", "fixtures", "test-200x150.png");
|
const testFixture = path.join(
|
||||||
|
process.cwd(),
|
||||||
|
"tests",
|
||||||
|
"fixtures",
|
||||||
|
"image",
|
||||||
|
"valid",
|
||||||
|
"test-200x150.png",
|
||||||
|
);
|
||||||
await page.locator("#compare-second-image").setInputFiles(testFixture);
|
await page.locator("#compare-second-image").setInputFiles(testFixture);
|
||||||
await page.waitForTimeout(300);
|
await page.waitForTimeout(300);
|
||||||
|
|
||||||
@@ -216,7 +223,14 @@ test.describe("GUI Expanded Tool Coverage", () => {
|
|||||||
await page.goto("/compare");
|
await page.goto("/compare");
|
||||||
await uploadTestImage(page);
|
await uploadTestImage(page);
|
||||||
|
|
||||||
const testFixture = path.join(process.cwd(), "tests", "fixtures", "test-200x150.png");
|
const testFixture = path.join(
|
||||||
|
process.cwd(),
|
||||||
|
"tests",
|
||||||
|
"fixtures",
|
||||||
|
"image",
|
||||||
|
"valid",
|
||||||
|
"test-200x150.png",
|
||||||
|
);
|
||||||
await page.locator("#compare-second-image").setInputFiles(testFixture);
|
await page.locator("#compare-second-image").setInputFiles(testFixture);
|
||||||
await page.waitForTimeout(300);
|
await page.waitForTimeout(300);
|
||||||
|
|
||||||
@@ -227,7 +241,14 @@ test.describe("GUI Expanded Tool Coverage", () => {
|
|||||||
await page.goto("/compare");
|
await page.goto("/compare");
|
||||||
await uploadTestImage(page);
|
await uploadTestImage(page);
|
||||||
|
|
||||||
const testFixture = path.join(process.cwd(), "tests", "fixtures", "test-200x150.png");
|
const testFixture = path.join(
|
||||||
|
process.cwd(),
|
||||||
|
"tests",
|
||||||
|
"fixtures",
|
||||||
|
"image",
|
||||||
|
"valid",
|
||||||
|
"test-200x150.png",
|
||||||
|
);
|
||||||
await page.locator("#compare-second-image").setInputFiles(testFixture);
|
await page.locator("#compare-second-image").setInputFiles(testFixture);
|
||||||
await page.waitForTimeout(300);
|
await page.waitForTimeout(300);
|
||||||
|
|
||||||
@@ -241,7 +262,14 @@ test.describe("GUI Expanded Tool Coverage", () => {
|
|||||||
await page.goto("/compare");
|
await page.goto("/compare");
|
||||||
await uploadTestImage(page);
|
await uploadTestImage(page);
|
||||||
|
|
||||||
const testFixture = path.join(process.cwd(), "tests", "fixtures", "test-200x150.png");
|
const testFixture = path.join(
|
||||||
|
process.cwd(),
|
||||||
|
"tests",
|
||||||
|
"fixtures",
|
||||||
|
"image",
|
||||||
|
"valid",
|
||||||
|
"test-200x150.png",
|
||||||
|
);
|
||||||
await page.locator("#compare-second-image").setInputFiles(testFixture);
|
await page.locator("#compare-second-image").setInputFiles(testFixture);
|
||||||
await page.waitForTimeout(300);
|
await page.waitForTimeout(300);
|
||||||
|
|
||||||
|
|||||||
@@ -86,7 +86,9 @@ test.describe("GUI Format & Conversion Tools", () => {
|
|||||||
const fileChooserPromise = page.waitForEvent("filechooser");
|
const fileChooserPromise = page.waitForEvent("filechooser");
|
||||||
await page.locator("[class*='border-dashed']").first().click();
|
await page.locator("[class*='border-dashed']").first().click();
|
||||||
const fileChooser = await fileChooserPromise;
|
const fileChooser = await fileChooserPromise;
|
||||||
await fileChooser.setFiles(path.join(process.cwd(), "tests", "fixtures", "test-100x100.svg"));
|
await fileChooser.setFiles(
|
||||||
|
path.join(process.cwd(), "tests", "fixtures", "image", "valid", "test-100x100.svg"),
|
||||||
|
);
|
||||||
await page.waitForTimeout(500);
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
await page.getByRole("button", { name: "Scale Factor" }).click();
|
await page.getByRole("button", { name: "Scale Factor" }).click();
|
||||||
@@ -294,7 +296,9 @@ test.describe("GUI Format & Conversion Tools", () => {
|
|||||||
const fileChooserPromise = page.waitForEvent("filechooser");
|
const fileChooserPromise = page.waitForEvent("filechooser");
|
||||||
await page.locator("[class*='border-dashed']").first().click();
|
await page.locator("[class*='border-dashed']").first().click();
|
||||||
const fileChooser = await fileChooserPromise;
|
const fileChooser = await fileChooserPromise;
|
||||||
await fileChooser.setFiles(path.join(process.cwd(), "tests", "fixtures", "animated.gif"));
|
await fileChooser.setFiles(
|
||||||
|
path.join(process.cwd(), "tests", "fixtures", "image", "valid", "animated.gif"),
|
||||||
|
);
|
||||||
await page.waitForTimeout(500);
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
await page.getByRole("button", { name: "Speed" }).first().click();
|
await page.getByRole("button", { name: "Speed" }).first().click();
|
||||||
@@ -308,7 +312,9 @@ test.describe("GUI Format & Conversion Tools", () => {
|
|||||||
const fileChooserPromise = page.waitForEvent("filechooser");
|
const fileChooserPromise = page.waitForEvent("filechooser");
|
||||||
await page.locator("[class*='border-dashed']").first().click();
|
await page.locator("[class*='border-dashed']").first().click();
|
||||||
const fileChooser = await fileChooserPromise;
|
const fileChooser = await fileChooserPromise;
|
||||||
await fileChooser.setFiles(path.join(process.cwd(), "tests", "fixtures", "animated.gif"));
|
await fileChooser.setFiles(
|
||||||
|
path.join(process.cwd(), "tests", "fixtures", "image", "valid", "animated.gif"),
|
||||||
|
);
|
||||||
await page.waitForTimeout(500);
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
await page.getByRole("button", { name: "Extract" }).first().click();
|
await page.getByRole("button", { name: "Extract" }).first().click();
|
||||||
|
|||||||
@@ -425,7 +425,14 @@ test.describe("GUI Watermark & Overlay Tools", () => {
|
|||||||
await page.goto("/compose");
|
await page.goto("/compose");
|
||||||
await uploadBaseImage(page);
|
await uploadBaseImage(page);
|
||||||
|
|
||||||
const webpPath = path.join(process.cwd(), "tests", "fixtures", "test-50x50.webp");
|
const webpPath = path.join(
|
||||||
|
process.cwd(),
|
||||||
|
"tests",
|
||||||
|
"fixtures",
|
||||||
|
"image",
|
||||||
|
"valid",
|
||||||
|
"test-50x50.webp",
|
||||||
|
);
|
||||||
await uploadOverlayImage(page, webpPath);
|
await uploadOverlayImage(page, webpPath);
|
||||||
|
|
||||||
await page.getByTestId("compose-submit").click();
|
await page.getByTestId("compose-submit").click();
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { expect, test, uploadTestImage, waitForProcessing } from "./helpers";
|
import { expect, test, uploadTestImage, waitForProcessing } from "./helpers";
|
||||||
|
|
||||||
const FIXTURE_PNG = path.join(process.cwd(), "tests", "fixtures", "test-200x150.png");
|
const FIXTURE_PNG = path.join(
|
||||||
|
process.cwd(),
|
||||||
|
"tests",
|
||||||
|
"fixtures",
|
||||||
|
"image",
|
||||||
|
"valid",
|
||||||
|
"test-200x150.png",
|
||||||
|
);
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// GUI E2E: Utility Tools
|
// GUI E2E: Utility Tools
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ export function getTestImagePath(): string {
|
|||||||
// getTestHeicPath() — return a small HEIC test image (from fixtures)
|
// getTestHeicPath() — return a small HEIC test image (from fixtures)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
export function getTestHeicPath(): string {
|
export function getTestHeicPath(): string {
|
||||||
return path.join(process.cwd(), "tests", "fixtures", "test-200x150.heic");
|
return path.join(process.cwd(), "tests", "fixtures", "image", "valid", "test-200x150.heic");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { expect, test, waitForProcessing } from "./helpers";
|
import { expect, test, waitForProcessing } from "./helpers";
|
||||||
|
|
||||||
const MP4_FIXTURE = path.join(process.cwd(), "tests", "fixtures", "media", "tiny.mp4");
|
const MP4_FIXTURE = path.join(process.cwd(), "tests", "fixtures", "video", "formats", "tiny.mp4");
|
||||||
|
|
||||||
test.describe("Media-player display mode (mute-video)", () => {
|
test.describe("Media-player display mode (mute-video)", () => {
|
||||||
test("uploads a video, mutes it, and shows the media player with processed result", async ({
|
test("uploads a video, mutes it, and shows the media player with processed result", async ({
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ test.describe("OCR / Text Extraction", () => {
|
|||||||
test("uploads image and OCR processing completes", async ({ loggedInPage: page }) => {
|
test("uploads image and OCR processing completes", async ({ loggedInPage: page }) => {
|
||||||
await skipIfFeatureNotInstalled(page);
|
await skipIfFeatureNotInstalled(page);
|
||||||
|
|
||||||
await uploadOcrFile(page, "tests/fixtures/test-portrait.jpg");
|
await uploadOcrFile(page, "tests/fixtures/image/valid/test-portrait.jpg");
|
||||||
await submitOcr(page);
|
await submitOcr(page);
|
||||||
|
|
||||||
const hasText = await page.getByTestId("ocr-result-text").isVisible();
|
const hasText = await page.getByTestId("ocr-result-text").isVisible();
|
||||||
@@ -92,7 +92,7 @@ test.describe("OCR / Text Extraction", () => {
|
|||||||
test("copy button is visible after OCR completes", async ({ loggedInPage: page }) => {
|
test("copy button is visible after OCR completes", async ({ loggedInPage: page }) => {
|
||||||
await skipIfFeatureNotInstalled(page);
|
await skipIfFeatureNotInstalled(page);
|
||||||
|
|
||||||
await uploadOcrFile(page, "tests/fixtures/test-portrait.jpg");
|
await uploadOcrFile(page, "tests/fixtures/image/valid/test-portrait.jpg");
|
||||||
await submitOcr(page);
|
await submitOcr(page);
|
||||||
|
|
||||||
await expect(page.getByText("Copy")).toBeVisible();
|
await expect(page.getByText("Copy")).toBeVisible();
|
||||||
@@ -101,7 +101,7 @@ test.describe("OCR / Text Extraction", () => {
|
|||||||
test("shows 'no text detected' for blank image", async ({ loggedInPage: page }) => {
|
test("shows 'no text detected' for blank image", async ({ loggedInPage: page }) => {
|
||||||
await skipIfFeatureNotInstalled(page);
|
await skipIfFeatureNotInstalled(page);
|
||||||
|
|
||||||
await uploadOcrFile(page, "tests/fixtures/test-blank.png");
|
await uploadOcrFile(page, "tests/fixtures/image/edge/test-blank.png");
|
||||||
await submitOcr(page);
|
await submitOcr(page);
|
||||||
|
|
||||||
await expect(page.getByText("No text detected")).toBeVisible();
|
await expect(page.getByText("No text detected")).toBeVisible();
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { expect, test, waitForProcessing } from "./helpers";
|
import { expect, test, waitForProcessing } from "./helpers";
|
||||||
|
|
||||||
const PDF_FIXTURE = path.join(process.cwd(), "tests", "fixtures", "test-3page.pdf");
|
const PDF_FIXTURE = path.join(
|
||||||
|
process.cwd(),
|
||||||
|
"tests",
|
||||||
|
"fixtures",
|
||||||
|
"document",
|
||||||
|
"valid",
|
||||||
|
"test-3page.pdf",
|
||||||
|
);
|
||||||
|
|
||||||
test.describe("PDF to Image tool", () => {
|
test.describe("PDF to Image tool", () => {
|
||||||
test("shows page thumbnails after uploading a PDF", async ({ loggedInPage: page }) => {
|
test("shows page thumbnails after uploading a PDF", async ({ loggedInPage: page }) => {
|
||||||
|
|||||||
@@ -133,7 +133,14 @@ test.describe("State bleed between tools", () => {
|
|||||||
await addMoreBtn.click();
|
await addMoreBtn.click();
|
||||||
const fileChooser = await fileChooserPromise;
|
const fileChooser = await fileChooserPromise;
|
||||||
await fileChooser.setFiles(
|
await fileChooser.setFiles(
|
||||||
require("node:path").join(process.cwd(), "tests", "fixtures", "test-50x50.webp"),
|
require("node:path").join(
|
||||||
|
process.cwd(),
|
||||||
|
"tests",
|
||||||
|
"fixtures",
|
||||||
|
"image",
|
||||||
|
"valid",
|
||||||
|
"test-50x50.webp",
|
||||||
|
),
|
||||||
);
|
);
|
||||||
await page.waitForTimeout(500);
|
await page.waitForTimeout(500);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
Phase 6 reorg moves audio fixtures here (valid/ formats/ edge/ hostile/). See ../README.md.
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
Phase 6 reorg moves data fixtures here (valid/ formats/ edge/ hostile/). See ../README.md.
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
Phase 6 reorg moves document fixtures here (valid/ formats/ edge/ hostile/). See ../README.md.
|
|
||||||
@@ -1,22 +1,33 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
// Stamps sha256 + bytes for the real `content/` heroes into manifest.json.
|
// Stamps sha256 + bytes for real fixture assets into manifest.json.
|
||||||
// Provenance (sourceUrl/license) is filled by hand in Step 2 -- this only fills hashes.
|
// Provenance (sourceUrl/license) is filled by hand -- this only fills hashes.
|
||||||
import { createHash } from "node:crypto";
|
import { createHash } from "node:crypto";
|
||||||
import { readdirSync, readFileSync, writeFileSync } from "node:fs";
|
import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||||
import { dirname, join } from "node:path";
|
import { dirname, join } from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
const ROOT = dirname(fileURLToPath(import.meta.url));
|
const ROOT = dirname(fileURLToPath(import.meta.url));
|
||||||
const contentDir = join(ROOT, "content");
|
|
||||||
const existing = JSON.parse(readFileSync(join(ROOT, "manifest.json"), "utf8"));
|
const existing = JSON.parse(readFileSync(join(ROOT, "manifest.json"), "utf8"));
|
||||||
const byPath = new Map(existing.assets.map((a) => [a.path, a]));
|
const byPath = new Map(existing.assets.map((a) => [a.path, a]));
|
||||||
|
|
||||||
for (const name of readdirSync(contentDir)) {
|
// Scan each modality valid dir (replaces the old content/ scan)
|
||||||
const rel = `content/${name}`;
|
const SCAN_DIRS = [
|
||||||
const bytes = readFileSync(join(contentDir, name));
|
"image/valid",
|
||||||
const sha256 = createHash("sha256").update(bytes).digest("hex");
|
"video/valid",
|
||||||
const prev = byPath.get(rel) ?? { path: rel, sourceUrl: "UNVERIFIED", license: "UNVERIFIED", toolsServed: [] };
|
"audio/valid",
|
||||||
byPath.set(rel, { ...prev, sha256, bytes: bytes.length });
|
"document/valid",
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const dir of SCAN_DIRS) {
|
||||||
|
const absDir = join(ROOT, dir);
|
||||||
|
if (!existsSync(absDir)) continue;
|
||||||
|
for (const name of readdirSync(absDir)) {
|
||||||
|
const rel = `${dir}/${name}`;
|
||||||
|
const bytes = readFileSync(join(absDir, name));
|
||||||
|
const sha256 = createHash("sha256").update(bytes).digest("hex");
|
||||||
|
const prev = byPath.get(rel) ?? { path: rel, sourceUrl: "UNVERIFIED", license: "UNVERIFIED", toolsServed: [] };
|
||||||
|
byPath.set(rel, { ...prev, sha256, bytes: bytes.length });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const assets = [...byPath.values()].sort((a, b) => a.path.localeCompare(b.path));
|
const assets = [...byPath.values()].sort((a, b) => a.path.localeCompare(b.path));
|
||||||
writeFileSync(join(ROOT, "manifest.json"), JSON.stringify({ assets }, null, 2));
|
writeFileSync(join(ROOT, "manifest.json"), JSON.stringify({ assets }, null, 2));
|
||||||
|
|||||||
@@ -21,7 +21,10 @@ import { fileURLToPath } from "node:url";
|
|||||||
import { createRequire } from "node:module";
|
import { createRequire } from "node:module";
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
const CONTENT = join(__dirname, "content");
|
const IMAGE_VALID = join(__dirname, "image/valid");
|
||||||
|
const VIDEO_VALID = join(__dirname, "video/valid");
|
||||||
|
const AUDIO_VALID = join(__dirname, "audio/valid");
|
||||||
|
const DOC_VALID = join(__dirname, "document/valid");
|
||||||
const require = createRequire(join(__dirname, "../../apps/api/package.json"));
|
const require = createRequire(join(__dirname, "../../apps/api/package.json"));
|
||||||
const sharp = require("sharp");
|
const sharp = require("sharp");
|
||||||
const QRCode = require("qrcode");
|
const QRCode = require("qrcode");
|
||||||
@@ -223,15 +226,15 @@ async function main() {
|
|||||||
const qrPng = await QRCode.toBuffer(qrData, {
|
const qrPng = await QRCode.toBuffer(qrData, {
|
||||||
width: 400, margin: 2, color: { dark: "#000000", light: "#ffffff" },
|
width: 400, margin: 2, color: { dark: "#000000", light: "#ffffff" },
|
||||||
});
|
});
|
||||||
writeFileSync(join(CONTENT, "qr-code.png"), qrPng);
|
writeFileSync(join(IMAGE_VALID, "qr-code.png"), qrPng);
|
||||||
console.log(" qr-code.png");
|
console.log(" qr-code.png");
|
||||||
|
|
||||||
const qrSvg = await QRCode.toString(qrData, { type: "svg", width: 296, margin: 2 });
|
const qrSvg = await QRCode.toString(qrData, { type: "svg", width: 296, margin: 2 });
|
||||||
writeFileSync(join(CONTENT, "qr-code.svg"), qrSvg);
|
writeFileSync(join(IMAGE_VALID, "qr-code.svg"), qrSvg);
|
||||||
console.log(" qr-code.svg");
|
console.log(" qr-code.svg");
|
||||||
|
|
||||||
const qrAvif = await sharp(qrPng).resize(400, 400).avif({ quality: 80 }).toBuffer();
|
const qrAvif = await sharp(qrPng).resize(400, 400).avif({ quality: 80 }).toBuffer();
|
||||||
writeFileSync(join(CONTENT, "qr-code.avif"), qrAvif);
|
writeFileSync(join(IMAGE_VALID, "qr-code.avif"), qrAvif);
|
||||||
console.log(" qr-code.avif");
|
console.log(" qr-code.avif");
|
||||||
|
|
||||||
// ── Barcodes (A) ──
|
// ── Barcodes (A) ──
|
||||||
@@ -239,11 +242,11 @@ async function main() {
|
|||||||
const bcText = "SNAPOTTER-TEST-123";
|
const bcText = "SNAPOTTER-TEST-123";
|
||||||
const bcSvgStr = barcodeSvg(bcText, 699, 152);
|
const bcSvgStr = barcodeSvg(bcText, 699, 152);
|
||||||
const bcPng = await sharp(Buffer.from(bcSvgStr)).png().toBuffer();
|
const bcPng = await sharp(Buffer.from(bcSvgStr)).png().toBuffer();
|
||||||
writeFileSync(join(CONTENT, "barcode.png"), bcPng);
|
writeFileSync(join(IMAGE_VALID, "barcode.png"), bcPng);
|
||||||
console.log(" barcode.png");
|
console.log(" barcode.png");
|
||||||
|
|
||||||
const bcAvif = await sharp(Buffer.from(bcSvgStr)).avif({ quality: 80 }).toBuffer();
|
const bcAvif = await sharp(Buffer.from(bcSvgStr)).avif({ quality: 80 }).toBuffer();
|
||||||
writeFileSync(join(CONTENT, "barcode.avif"), bcAvif);
|
writeFileSync(join(IMAGE_VALID, "barcode.avif"), bcAvif);
|
||||||
console.log(" barcode.avif");
|
console.log(" barcode.avif");
|
||||||
|
|
||||||
// ── OCR text images (A) ──
|
// ── OCR text images (A) ──
|
||||||
@@ -253,7 +256,7 @@ async function main() {
|
|||||||
<text x="400" y="115" text-anchor="middle" font-family="Helvetica, Arial, sans-serif" font-size="42" font-weight="bold" fill="black">The quick brown fox 12345</text>
|
<text x="400" y="115" text-anchor="middle" font-family="Helvetica, Arial, sans-serif" font-size="42" font-weight="bold" fill="black">The quick brown fox 12345</text>
|
||||||
</svg>`;
|
</svg>`;
|
||||||
const ocrEngPng = await sharp(Buffer.from(ocrEngSvg)).png().toBuffer();
|
const ocrEngPng = await sharp(Buffer.from(ocrEngSvg)).png().toBuffer();
|
||||||
writeFileSync(join(CONTENT, "ocr-clean.png"), ocrEngPng);
|
writeFileSync(join(IMAGE_VALID, "ocr-clean.png"), ocrEngPng);
|
||||||
console.log(" ocr-clean.png");
|
console.log(" ocr-clean.png");
|
||||||
|
|
||||||
const jpText =
|
const jpText =
|
||||||
@@ -273,31 +276,31 @@ async function main() {
|
|||||||
${lines.map((line, i) => `<text x="16" y="${30 + i * 28}" font-family="Hiragino Sans, Hiragino Kaku Gothic Pro, sans-serif" font-size="20" fill="black">${line}</text>`).join("\n ")}
|
${lines.map((line, i) => `<text x="16" y="${30 + i * 28}" font-family="Hiragino Sans, Hiragino Kaku Gothic Pro, sans-serif" font-size="20" fill="black">${line}</text>`).join("\n ")}
|
||||||
</svg>`;
|
</svg>`;
|
||||||
const jpPng = await sharp(Buffer.from(jpSvg)).png().toBuffer();
|
const jpPng = await sharp(Buffer.from(jpSvg)).png().toBuffer();
|
||||||
writeFileSync(join(CONTENT, "ocr-japanese.png"), jpPng);
|
writeFileSync(join(IMAGE_VALID, "ocr-japanese.png"), jpPng);
|
||||||
console.log(" ocr-japanese.png");
|
console.log(" ocr-japanese.png");
|
||||||
|
|
||||||
// ── PDFs (A) ──
|
// ── PDFs (A) ──
|
||||||
console.log("PDFs:");
|
console.log("PDFs:");
|
||||||
writeFileSync(join(CONTENT, "alt-2page.pdf"), generatePdf(2));
|
writeFileSync(join(DOC_VALID, "alt-2page.pdf"), generatePdf(2));
|
||||||
console.log(" alt-2page.pdf");
|
console.log(" alt-2page.pdf");
|
||||||
writeFileSync(join(CONTENT, "multipage-6.pdf"), generatePdf(6));
|
writeFileSync(join(DOC_VALID, "multipage-6.pdf"), generatePdf(6));
|
||||||
console.log(" multipage-6.pdf");
|
console.log(" multipage-6.pdf");
|
||||||
|
|
||||||
// ── SVG logo (B: replaces ConvertICO brand) ──
|
// ── SVG logo (B: replaces ConvertICO brand) ──
|
||||||
console.log("SVG logo (replacing ConvertICO brand):");
|
console.log("SVG logo (replacing ConvertICO brand):");
|
||||||
writeFileSync(join(CONTENT, "svg-logo.svg"), projectSvg());
|
writeFileSync(join(IMAGE_VALID, "svg-logo.svg"), projectSvg());
|
||||||
console.log(" svg-logo.svg");
|
console.log(" svg-logo.svg");
|
||||||
|
|
||||||
// ── Multi-face placeholder (B: replaces Shutterstock photo) ──
|
// ── Multi-face placeholder (B: replaces Shutterstock photo) ──
|
||||||
console.log("Multi-face placeholder (replacing Shutterstock stock photo):");
|
console.log("Multi-face placeholder (replacing Shutterstock stock photo):");
|
||||||
const mfSvg = multiFaceSvg(433, 280);
|
const mfSvg = multiFaceSvg(433, 280);
|
||||||
const mfWebp = await sharp(Buffer.from(mfSvg)).webp({ quality: 80 }).toBuffer();
|
const mfWebp = await sharp(Buffer.from(mfSvg)).webp({ quality: 80 }).toBuffer();
|
||||||
writeFileSync(join(CONTENT, "multi-face.webp"), mfWebp);
|
writeFileSync(join(IMAGE_VALID, "multi-face.webp"), mfWebp);
|
||||||
console.log(" multi-face.webp");
|
console.log(" multi-face.webp");
|
||||||
|
|
||||||
// ── Animated GIF (B: replaces copyrighted Simpsons clip) ──
|
// ── Animated GIF (B: replaces copyrighted Simpsons clip) ──
|
||||||
console.log("Animated GIF (replacing copyrighted Simpsons clip):");
|
console.log("Animated GIF (replacing copyrighted Simpsons clip):");
|
||||||
const gifOut = join(CONTENT, "animated-simpsons.gif");
|
const gifOut = join(IMAGE_VALID, "animated-simpsons.gif");
|
||||||
ff(
|
ff(
|
||||||
`-f lavfi -i "testsrc=duration=3:size=320x320:rate=10" -pix_fmt rgb8 -loop 0 -y "${gifOut}"`,
|
`-f lavfi -i "testsrc=duration=3:size=320x320:rate=10" -pix_fmt rgb8 -loop 0 -y "${gifOut}"`,
|
||||||
"animated-simpsons.gif",
|
"animated-simpsons.gif",
|
||||||
@@ -312,14 +315,14 @@ async function main() {
|
|||||||
`-metadata title="Test Song" -metadata artist="Test Artist" ` +
|
`-metadata title="Test Song" -metadata artist="Test Artist" ` +
|
||||||
`-metadata album="Test Album" -metadata date="2026" ` +
|
`-metadata album="Test Album" -metadata date="2026" ` +
|
||||||
`-metadata genre="Electronic" -metadata track="1/10" ` +
|
`-metadata genre="Electronic" -metadata track="1/10" ` +
|
||||||
`-y "${join(CONTENT, "audio-with-tags.mp3")}"`,
|
`-y "${join(AUDIO_VALID, "audio-with-tags.mp3")}"`,
|
||||||
"audio-with-tags.mp3",
|
"audio-with-tags.mp3",
|
||||||
);
|
);
|
||||||
|
|
||||||
ff(
|
ff(
|
||||||
`-f lavfi -i "color=c=0xE07832:s=64x64:d=1:rate=16" ` +
|
`-f lavfi -i "color=c=0xE07832:s=64x64:d=1:rate=16" ` +
|
||||||
`-c:v libx264 -pix_fmt yuv420p -movflags +faststart ` +
|
`-c:v libx264 -pix_fmt yuv420p -movflags +faststart ` +
|
||||||
`-y "${join(CONTENT, "video-with-meta.mp4")}"`,
|
`-y "${join(VIDEO_VALID, "video-with-meta.mp4")}"`,
|
||||||
"video-with-meta.mp4",
|
"video-with-meta.mp4",
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -328,14 +331,14 @@ async function main() {
|
|||||||
`-f lavfi -i "sine=frequency=440:duration=30:sample_rate=44100" ` +
|
`-f lavfi -i "sine=frequency=440:duration=30:sample_rate=44100" ` +
|
||||||
`-c:v libx264 -pix_fmt yuv420p -preset ultrafast ` +
|
`-c:v libx264 -pix_fmt yuv420p -preset ultrafast ` +
|
||||||
`-c:a aac -b:a 128k -ac 1 -movflags +faststart ` +
|
`-c:a aac -b:a 128k -ac 1 -movflags +faststart ` +
|
||||||
`-y "${join(CONTENT, "media-30s.mp4")}"`,
|
`-y "${join(VIDEO_VALID, "media-30s.mp4")}"`,
|
||||||
"media-30s.mp4",
|
"media-30s.mp4",
|
||||||
);
|
);
|
||||||
|
|
||||||
ff(
|
ff(
|
||||||
`-f lavfi -i "sine=frequency=440:duration=30:sample_rate=44100" ` +
|
`-f lavfi -i "sine=frequency=440:duration=30:sample_rate=44100" ` +
|
||||||
`-ac 1 -c:a pcm_s16le ` +
|
`-ac 1 -c:a pcm_s16le ` +
|
||||||
`-y "${join(CONTENT, "media-30s.wav")}"`,
|
`-y "${join(AUDIO_VALID, "media-30s.wav")}"`,
|
||||||
"media-30s.wav",
|
"media-30s.wav",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
Phase 6 reorg moves image fixtures here (valid/ formats/ edge/ hostile/). See ../README.md.
|
|
||||||
|
Before Width: | Height: | Size: 91 B After Width: | Height: | Size: 91 B |
|
Before Width: | Height: | Size: 287 B After Width: | Height: | Size: 287 B |
|
Before Width: | Height: | Size: 1.6 MiB After Width: | Height: | Size: 1.6 MiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 511 KiB After Width: | Height: | Size: 511 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 799 KiB After Width: | Height: | Size: 799 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 190 KiB After Width: | Height: | Size: 190 KiB |