// Per-tool x per-format PROCESSING coverage is handled by the separate API sweep
// (api-sweep.mts). This spec covers INPUT PREVIEW rendering: every (modality x
// format) combination once on a representative standard tool, plus every
// custom-displayMode tool individually.
//
// Input preview is tool-independent for standard displayMode tools (all image
// tools share ImageViewer, all video share the media player, etc.), so we test
// each format once on a representative tool rather than on every tool.
import { expect, type Page, test } from "@playwright/test";
import type { PageIssues } from "./qa-helpers";
import {
assertAudioPreview,
assertDocumentPreview,
assertImageRendered,
assertNoBrokenImages,
assertVideoPreview,
fixture,
gotoTool,
instrument,
isClean,
issuesSummary,
uploadFiles,
} from "./qa-helpers";
// ---------------------------------------------------------------------------
// Custom oracles
// ---------------------------------------------------------------------------
/**
* Server-decode image preview: the browser cannot decode the raw blob, so the
* app must either (a) produce a server-decoded preview (img naturalWidth > 0)
* or (b) show a graceful fallback message. A raw blob
stuck at
* naturalWidth==0 with NO fallback is BROKEN.
*/
async function assertServerDecodePreview(
page: Page,
fixtureName: string,
): Promise<"rendered" | "fallback"> {
const img = page.locator(`img[alt="${fixtureName}"]`).first();
const fallback = page
.getByText(/generating preview|preview not available|cannot preview|processing/i)
.first();
const deadline = Date.now() + 15_000;
while (Date.now() < deadline) {
if (await img.isVisible().catch(() => false)) {
const w = await img.evaluate((el: HTMLImageElement) => el.naturalWidth).catch(() => 0);
if (w > 0) return "rendered";
}
if (await fallback.isVisible().catch(() => false)) return "fallback";
await page.waitForTimeout(400);
}
// Final check after timeout
if (await img.isVisible().catch(() => false)) {
const w = await img.evaluate((el: HTMLImageElement) => el.naturalWidth).catch(() => 0);
if (w > 0) return "rendered";
throw new Error(
`BROKEN: img[alt="${fixtureName}"] visible but naturalWidth=0, no fallback shown`,
);
}
if (await fallback.isVisible().catch(() => false)) return "fallback";
throw new Error(`BROKEN: no preview img or fallback message for "${fixtureName}" after 15s`);
}
/**
* Video non-native: the browser may not decode MKV/AVI/etc directly. The app
* should either play the video (Chrome sometimes can) OR show a "Generate
* Preview" button. Neither = BROKEN.
*/
async function assertVideoNonNativePreview(page: Page): Promise {
const video = page.getByTestId("media-player-video").first();
const generateBtn = page.getByRole("button", { name: /generate preview/i }).first();
const fallbackText = page
.getByText(/preview not available|cannot preview|unsupported format/i)
.first();
const deadline = Date.now() + 12_000;
while (Date.now() < deadline) {
if (await video.isVisible().catch(() => false)) {
const state = await video.evaluate((el: HTMLVideoElement) => el.readyState).catch(() => 0);
if (state >= 1) return;
}
if (await generateBtn.isVisible().catch(() => false)) return;
if (await fallbackText.isVisible().catch(() => false)) return;
await page.waitForTimeout(400);
}
throw new Error(
"BROKEN: neither video player (readyState>=1) nor Generate Preview button visible",
);
}
/**
* Non-PDF document on a "document" displayMode tool: pdf.js cannot render it.
* Check for graceful handling vs ugly "Load failed" error.
*/
async function assertNonPdfDocumentPreview(page: Page): Promise {
await page.waitForTimeout(4_000);
const destructive = page.locator(".text-destructive");
const count = await destructive.count();
for (let i = 0; i < count; i++) {
const text = (
await destructive
.nth(i)
.innerText()
.catch(() => "")
).trim();
if (/failed to load|load failed|error loading|invalid pdf/i.test(text)) {
throw new Error(`Non-PDF document shows ugly pdf.js error: "${text.substring(0, 200)}"`);
}
}
}
/**
* Interactive tool preview: image may be on an
(any alt), a Konva
* canvas, or a plain