Files
goon_plugin/__tests__/viewer_units.test.mjs
hermes 90c3425200
CI / test (pull_request) Canceled after 0s
feat: add HLS playback support with hls.js
- viewer.js: dynamic hls.js loading, HLS-aware panel creation, error handling
- schema.json: hlsManifest and mediaType fields (optional, backward compatible)
- types.ts: VideoEntry interface with MediaType type
- isHls() null guard fix
- tests: 39 tests (schema, viewer units, data flow, integration)
- CI: GitHub/Gitea Actions workflow (npm test on push/PR)
- deps: ajv, ajv-formats, jsdom for testing
2026-08-10 06:18:27 +00:00

193 lines
6.4 KiB
JavaScript

import { describe, it } from './runner.mjs';
import { JSDOM } from 'jsdom';
describe('Viewer: resolveSrc()', () => {
// We extract the pure logic functions from viewer.js for unit testing.
// Since viewer.js uses browser globals, we test the logic independently.
const resolveSrc = (entry) => {
if (typeof entry === 'string') return entry;
return entry.src || entry.url || '';
};
it('should resolve string entries directly', () => {
const result = resolveSrc('https://example.com/video.mp4');
if (result !== 'https://example.com/video.mp4') {
throw new Error(`Expected original string, got: ${result}`);
}
});
it('should prefer src over url in object entries', () => {
const entry = { url: 'https://example.com/page', src: 'https://cdn.example.com/stream.mp4' };
const result = resolveSrc(entry);
if (result !== 'https://cdn.example.com/stream.mp4') {
throw new Error(`Expected src, got: ${result}`);
}
});
it('should fall back to url when src is missing', () => {
const entry = { url: 'https://example.com/video.mp4' };
const result = resolveSrc(entry);
if (result !== 'https://example.com/video.mp4') {
throw new Error(`Expected url, got: ${result}`);
}
});
it('should return empty string for empty object', () => {
const entry = {};
const result = resolveSrc(entry);
if (result !== '') {
throw new Error(`Expected empty string, got: ${result}`);
}
});
});
describe('Viewer: isHls()', () => {
const isHls = (entry) => {
return entry !== null && typeof entry === 'object' && entry.mediaType === 'hls';
};
it('should detect HLS entries with mediaType=hls', () => {
const entry = { url: 'https://example.com/stream', mediaType: 'hls', hlsManifest: 'https://cdn.example.com/stream.m3u8' };
if (!isHls(entry)) {
throw new Error('HLS entry not detected');
}
});
it('should NOT detect direct mediaType', () => {
const entry = { url: 'https://example.com/video.mp4', mediaType: 'direct' };
if (isHls(entry)) {
throw new Error('Direct entry falsely detected as HLS');
}
});
it('should NOT detect entries without mediaType (backward compat)', () => {
const entry = { url: 'https://example.com/video.mp4' };
if (isHls(entry)) {
throw new Error('Legacy entry without mediaType falsely detected as HLS');
}
});
it('should NOT detect string entries', () => {
if (isHls('https://example.com/video.mp4')) {
throw new Error('String entry falsely detected as HLS');
}
});
it('should NOT detect null entries (null guard)', () => {
// With the null guard, isHls(null) safely returns false instead of throwing
if (isHls(null)) {
throw new Error('Null entry falsely detected as HLS');
}
});
});
describe('Viewer: resolveHlsManifest()', () => {
const resolveHlsManifest = (entry) => {
if (typeof entry === 'object' && entry.hlsManifest) {
return entry.hlsManifest;
}
return null;
};
it('should return hlsManifest URL when present', () => {
const entry = { url: 'https://example.com/page', hlsManifest: 'https://cdn.example.com/stream.m3u8' };
const result = resolveHlsManifest(entry);
if (result !== 'https://cdn.example.com/stream.m3u8') {
throw new Error(`Expected hlsManifest URL, got: ${result}`);
}
});
it('should return null when hlsManifest is missing', () => {
const entry = { url: 'https://example.com/video.mp4' };
const result = resolveHlsManifest(entry);
if (result !== null) {
throw new Error(`Expected null, got: ${result}`);
}
});
it('should return null for string entries', () => {
const result = resolveHlsManifest('https://example.com/video.mp4');
if (result !== null) {
throw new Error(`Expected null for string, got: ${result}`);
}
});
});
describe('Viewer: DOM setup via jsdom', () => {
it('should create panels container and video elements', () => {
const dom = new JSDOM(`
<!DOCTYPE html>
<html><body><div id="panels"></div></body></html>
`);
const panelsEl = dom.window.document.getElementById('panels');
if (!panelsEl) {
throw new Error('panels element not found');
}
// Simulate what viewer.js does: create panels
const panel = dom.window.document.createElement('div');
panel.classList.add('panel');
const video = dom.window.document.createElement('video');
video.autoplay = true;
video.muted = true;
video.loop = true;
video.playsInline = true;
video.src = 'https://example.com/video.mp4';
panel.appendChild(video);
panelsEl.appendChild(panel);
const videos = panelsEl.querySelectorAll('video');
if (videos.length !== 1) {
throw new Error(`Expected 1 video, got ${videos.length}`);
}
if (!videos[0].autoplay) throw new Error('autoplay not set');
if (!videos[0].muted) throw new Error('muted not set');
if (!videos[0].loop) throw new Error('loop not set');
if (!videos[0].playsInline) throw new Error('playsInline not set');
});
it('should create an NxN grid layout', () => {
const dom = new JSDOM(`
<!DOCTYPE html>
<html><body><div id="panels"></div></body></html>
`);
const panelsEl = dom.window.document.getElementById('panels');
const gridN = 3;
for (let i = 0; i < gridN * gridN; i++) {
const panel = dom.window.document.createElement('div');
panel.classList.add('panel');
panelsEl.appendChild(panel);
}
panelsEl.style.gridTemplateColumns = `repeat(${gridN}, 1fr)`;
panelsEl.style.gridTemplateRows = `repeat(${gridN}, 1fr)`;
const panels = panelsEl.querySelectorAll('.panel');
if (panels.length !== 9) {
throw new Error(`Expected 9 panels, got ${panels.length}`);
}
if (panelsEl.style.gridTemplateColumns !== 'repeat(3, 1fr)') {
throw new Error('Grid columns not set correctly');
}
});
it('should handle empty panels correctly', () => {
const dom = new JSDOM(`
<!DOCTYPE html>
<html><body><div id="panels"></div></body></html>
`);
const panelsEl = dom.window.document.getElementById('panels');
const panel = dom.window.document.createElement('div');
panel.classList.add('panel');
panel.classList.add('empty');
panelsEl.appendChild(panel);
const emptyPanels = panelsEl.querySelectorAll('.panel.empty');
if (emptyPanels.length !== 1) {
throw new Error(`Expected 1 empty panel, got ${emptyPanels.length}`);
}
});
});