Files
ObsiGate/tests/frontend/media-viewer.test.mjs
T
bruno e2417cb5ab
CI / lint (push) Successful in 1m58s
CI / security (push) Successful in 1m22s
CI / test (push) Successful in 4m32s
CI / build (push) Successful in 2m6s
CI / e2e (push) Successful in 12m57s
feat: support audio & vidéo — lecteurs HTML5 intégrés #109
2026-09-23 09:14:34 -04:00

142 lines
6.2 KiB
JavaScript

#!/usr/bin/env node
/**
* ObsiGate — Audio/video viewer tests (roadmap #109).
*
* Pure helpers (formatMediaDuration / buildMediaUrl) are evaluated in a VM
* sandbox to avoid importing the full browser-only viewer chain. The rest are
* static checks on the source, CSS and backend wiring.
*
* Usage: node tests/frontend/media-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 sw = readFileSync(path.join(ROOT, "frontend", "sw.js"), "utf8");
const main = readFileSync(path.join(ROOT, "backend", "main.py"), "utf8");
const fr = readFileSync(path.join(ROOT, "frontend", "locales", "fr.json"), "utf8");
const en = readFileSync(path.join(ROOT, "frontend", "locales", "en.json"), "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("export function formatMediaDuration");
const end = viewer.indexOf("function buildDownloadUrl");
assert.ok(begin !== -1 && end > begin, "media viewer helper block not found");
const sandbox = {};
vm.createContext(sandbox);
vm.runInContext(
`${viewer.slice(begin, end).replace(/\bexport /g, "")}\nglobalThis.formatMediaDuration = formatMediaDuration; globalThis.buildMediaUrl = buildMediaUrl;`,
sandbox,
);
const { formatMediaDuration, buildMediaUrl } = sandbox;
test("formatMediaDuration renders m:ss and h:mm:ss", () => {
assert.equal(formatMediaDuration(0), "0:00");
assert.equal(formatMediaDuration(65), "1:05");
assert.equal(formatMediaDuration(3725), "1:02:05");
assert.equal(formatMediaDuration(NaN), "");
assert.equal(formatMediaDuration(-1), "");
});
test("buildMediaUrl encodes vault and path", () => {
const url = buildMediaUrl("My Vault", "café clip.mp4");
assert.equal(url, "/api/media/My%20Vault?path=caf%C3%A9%20clip.mp4");
});
// ── Static checks: viewer wiring ───────────────────────────────────────────
test("viewer.js dispatches is_audio / is_video to the media viewers", () => {
assert.match(viewer, /if \(data\.is_audio\) \{\s*renderAudioViewer\(area, data\);/);
assert.match(viewer, /if \(data\.is_video\) \{\s*renderVideoViewer\(area, data\);/);
});
test("audio viewer uses a native <audio> element and stream URL", () => {
const start = viewer.indexOf("export function renderAudioViewer");
const stop = viewer.indexOf("export function renderVideoViewer");
assert.ok(start !== -1 && stop > start, "renderAudioViewer block not found");
const block = viewer.slice(start, stop);
assert.match(block, /el\("audio"/);
assert.match(block, /controls: ""/);
assert.match(block, /data\.stream_url \|\| buildMediaUrl/);
});
test("video viewer uses a native <video> element with playsinline", () => {
const start = viewer.indexOf("export function renderVideoViewer");
const stop = viewer.indexOf("export function renderFile");
assert.ok(start !== -1 && stop > start, "renderVideoViewer block not found");
const block = viewer.slice(start, stop);
assert.match(block, /el\("video"/);
assert.match(block, /playsinline: ""/);
});
test("unsupported codec triggers the binary fallback UI", () => {
assert.match(viewer, /audio\.addEventListener\("error",\s*\(\)\s*=>\s*\{\s*renderMediaFallback/);
assert.match(viewer, /video\.addEventListener\("error",\s*\(\)\s*=>\s*\{\s*renderMediaFallback/);
assert.match(viewer, /export function renderMediaFallback/);
});
test("playback is stopped when the content area is re-rendered", () => {
assert.match(viewer, /_mediaViewerCleanup/);
assert.match(viewer, /_mediaViewerCleanup\(\); _mediaViewerCleanup = null;/);
});
// ── Static checks: CSS ─────────────────────────────────────────────────────
test("style.css defines the audio/video viewer layout", () => {
assert.match(css, /\.audio-viewer-container/);
assert.match(css, /\.audio-stage\s*\{/);
assert.match(css, /\.audio-artwork\s*\{/);
assert.match(css, /\.video-stage\s*\{/);
assert.match(css, /\.video-player\s*\{/);
});
// ── Static checks: icons, i18n, service worker ─────────────────────────────
test("utils.js maps media extensions to audio-lines / video icons", () => {
assert.match(utils, /"\.mp3": "audio-lines"/);
assert.match(utils, /"\.flac": "audio-lines"/);
assert.match(utils, /"\.mp4": "video"/);
assert.match(utils, /"\.webm": "video"/);
});
test("media viewer strings exist in FR and EN", () => {
for (const key of ["viewer.media_unsupported", "viewer.media_too_large"]) {
assert.match(fr, new RegExp(`"${key}"`));
assert.match(en, new RegExp(`"${key}"`));
}
});
test("service worker never caches streamed media", () => {
assert.match(sw, /startsWith\('\/api\/media\/'\)/);
assert.match(sw, /endsWith\('\/thumb'\)/);
});
// ── Static checks: backend ─────────────────────────────────────────────────
test("backend exposes /api/media and the shared Range helper", () => {
assert.match(main, /@app\.get\("\/api\/media\/\{vault_name\}"/);
assert.match(main, /def _stream_file_with_range\(/);
assert.match(main, /AUDIO_EXTENSIONS|is_audio/);
assert.match(main, /is_audio\(ext\) or is_video\(ext\)/);
});
if (process.exitCode) {
console.error("\nMedia viewer tests FAILED");
} else {
console.log("\nAll media viewer tests passed.");
}