Files
ObsiGate/tests/frontend/image-viewer.test.mjs
T
bruno 8264e7ffae
CI / lint (push) Successful in 2m1s
CI / security (push) Successful in 1m23s
CI / test (push) Successful in 4m27s
CI / build (push) Successful in 1m19s
CI / e2e (push) Successful in 17m45s
fix: conserver plein ecran et panneau metadonnees dans la visionneuse d'images (sidebar droite) + lanceur E2E Windows BUG-072
2026-09-23 13:43:47 -04:00

147 lines
6.7 KiB
JavaScript

#!/usr/bin/env node
/**
* ObsiGate — Image viewer tests (roadmap #108-D).
*
* Pure helpers (clampImageZoom / isImagePath / buildImageUrl) are evaluated in
* a VM sandbox to avoid importing the full browser-only viewer chain. The rest
* are static checks on the source and CSS.
*
* Usage: node tests/frontend/image-viewer.test.mjs
*/
import { strict as assert } from "node:assert";
import { readFileSync } from "node:fs";
import vm from "node:vm";
import path from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.join(__dirname, "..", "..");
const viewer = readFileSync(path.join(ROOT, "frontend", "js", "viewer.js"), "utf8");
const utils = readFileSync(path.join(ROOT, "frontend", "js", "utils.js"), "utf8");
const css = readFileSync(path.join(ROOT, "frontend", "style.css"), "utf8");
const main = readFileSync(path.join(ROOT, "backend", "main.py"), "utf8");
function test(label, fn) {
try {
fn();
console.log(" \u2713 " + label);
} catch (err) {
console.error(" \u2717 " + label + "\n " + String(err.message).slice(0, 300));
process.exitCode = 1;
}
}
// ── Pure helpers (VM sandbox) ──────────────────────────────────────────────
const begin = viewer.indexOf("const IMAGE_EXTS");
const end = viewer.indexOf("function formatBytes");
assert.ok(begin !== -1 && end !== -1, "image viewer helper block not found");
const sandbox = {};
vm.createContext(sandbox);
vm.runInContext(
`${viewer.slice(begin, end).replace(/\bexport /g, "")}\nglobalThis.clampImageZoom = clampImageZoom; globalThis.isImagePath = isImagePath; globalThis.buildImageUrl = buildImageUrl;`,
sandbox,
);
const { clampImageZoom, isImagePath, buildImageUrl } = sandbox;
test("clampImageZoom bounds to [0.1, 8]", () => {
assert.equal(clampImageZoom(0.01), 0.1);
assert.equal(clampImageZoom(100), 8);
assert.equal(clampImageZoom(2), 2);
assert.equal(clampImageZoom(NaN), 1);
});
test("isImagePath recognises image extensions (case-insensitive)", () => {
for (const p of ["a.png", "a.JPG", "dir/b.jpeg", "x/y/z.svg", "p.webp", "q.bmp", "r.ico", "g.gif"]) {
assert.equal(isImagePath(p), true, p);
}
for (const p of ["a.md", "a.txt", "noext", "a.PDF", ""]) {
assert.equal(isImagePath(p), false, p);
}
});
test("buildImageUrl encodes vault and path", () => {
const url = buildImageUrl("My Vault", "café image.png");
assert.equal(url, "/api/image/My%20Vault?path=caf%C3%A9%20image.png");
});
// ── Static checks: viewer wiring ───────────────────────────────────────────
test("viewer.js dispatches is_image to renderImageViewer", () => {
assert.match(viewer, /if \(data\.is_image\) \{\s*renderImageViewer\(area, data\);/);
});
test("renderImageViewer never uses the JSON raw endpoint as an image src", () => {
const start = viewer.indexOf("export function renderImageViewer");
const stop = viewer.indexOf("export function renderFile");
assert.ok(start !== -1 && stop > start, "renderImageViewer block not found");
const block = viewer.slice(start, stop);
assert.doesNotMatch(block, /\/raw\?path=/, "images must be served by /api/image, not /raw");
assert.match(block, /buildImageUrl\(vault, path\)/);
});
test("image viewer supports wheel zoom, drag pan and double-click reset", () => {
assert.match(viewer, /stage\.addEventListener\("wheel"/);
assert.match(viewer, /stage\.addEventListener\("pointerdown"/);
assert.match(viewer, /stage\.addEventListener\("dblclick", resetView\)/);
assert.match(viewer, /renderImageViewer/);
});
test("image viewer exposes thumbnail navigation via /api/media thumb", () => {
assert.match(viewer, /\/api\/media\/\$\{encodeURIComponent\(vault\)\}\/thumb\?path=/);
assert.match(viewer, /let siblings = \[\]/);
assert.match(viewer, /siblings = \(res\.items \|\| \[\]\)/);
});
// BUG-072 — lightbox + metadata panel must survive ←/→ (and filmstrip) navigation.
test("image viewer persists lightbox + metadata state across navigation", () => {
assert.match(viewer, /const _imageViewerState = \{ lightbox: false, meta: false \}/);
assert.match(viewer, /let _imageViewerNavPending = false/);
// Both navigation paths flag the next render as an image→image navigation.
assert.match(viewer, /_imageViewerNavPending = true;\s*\n\s*openFile\(vault, siblings\[next\]\.path\)/);
assert.match(viewer, /_imageViewerNavPending = true;\s*\n\s*openFile\(vault, s\.path\)/);
// renderFile resets the state only when the next render is not a carried-over image.
assert.match(viewer, /if \(!data\.is_image \|\| !_imageViewerNavPending\)/);
// The persisted state drives the container class and the panel visibility.
assert.match(viewer, /classList\.toggle\("lightbox", _imageViewerState\.lightbox\)/);
assert.match(viewer, /metaPanel\.hidden = !_imageViewerState\.meta/);
});
// ── Static checks: CSS ─────────────────────────────────────────────────────
test("style.css defines the image viewer layout + lightbox", () => {
assert.match(css, /\.image-stage\s*\{/);
assert.match(css, /\.image-nav-strip\s*\{/);
assert.match(css, /\.image-meta-panel\s*\{/);
assert.match(css, /\.image-viewer-container\.lightbox\s*\{/);
});
// BUG-072 — metadata is a right-hand sidebar, not a strip under the filmstrip,
// and it stays visible in lightbox mode.
test("style.css lays the metadata panel out as a right sidebar", () => {
assert.match(css, /\.image-viewer-body\s*\{[\s\S]*?display:\s*flex/);
const meta = /\.image-meta-panel\s*\{([^}]*)\}/.exec(css);
assert.ok(meta, ".image-meta-panel rule not found");
assert.match(meta[1], /border-left:/);
assert.doesNotMatch(meta[1], /border-top:/);
// The lightbox hide rule targets the filmstrip only — not the metadata sidebar.
assert.match(css, /\.image-viewer-container\.lightbox \.image-nav-strip\s*\{/);
assert.doesNotMatch(css, /\.image-viewer-container\.lightbox[^{]*\.image-meta-panel/);
});
// ── Static checks: icon mapping + backend ──────────────────────────────────
test("utils.js maps image extensions to the Lucide 'image' icon", () => {
assert.match(utils, /"\.png": "image"/);
assert.match(utils, /"\.svg": "image"/);
});
test("backend api_file_view points <img> at /api/image", () => {
assert.match(main, /img_url = f"\/api\/image\//);
assert.match(main, /f'<img src="\{img_url\}"/);
});
if (process.exitCode) {
console.error("\nImage viewer tests FAILED");
} else {
console.log("\nAll image viewer tests passed.");
}