CI / test (pull_request) Canceled after 0s
- 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
193 lines
6.4 KiB
JavaScript
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}`);
|
|
}
|
|
});
|
|
});
|