120 lines
5.0 KiB
JavaScript
120 lines
5.0 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 \|\| \[\]\)/);
|
|
});
|
|
|
|
// ── 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*\{/);
|
|
});
|
|
|
|
// ── 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.");
|
|
}
|