- 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
This commit is contained in:
1 parent
baa72a4c33
commit
90c3425200
12 files changed
+2295
-27
No files matched your search
@@ -0,0 +1,192 @@
|
||||
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}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user