mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-28 00:12:25 +00:00
196 lines
6.0 KiB
JavaScript
196 lines
6.0 KiB
JavaScript
import test, { beforeEach, afterEach, describe, it } from 'node:test';
|
|
import { strict as assert } from 'node:assert';
|
|
import { JSDOM } from 'jsdom';
|
|
import UrlHealthController from '../../js/ui/urlHealthController.js';
|
|
|
|
let dom;
|
|
let controller;
|
|
let mockCache = new Map();
|
|
let inFlightProbes = new Map();
|
|
|
|
const mockState = {
|
|
getCachedUrlHealth: (eventId, url) => mockCache.get(`${eventId}-${url}`),
|
|
storeUrlHealth: (eventId, url, result) => {
|
|
mockCache.set(`${eventId}-${url}`, result);
|
|
return result;
|
|
},
|
|
getInFlightUrlProbe: (eventId, url) => inFlightProbes.get(`${eventId}-${url}`),
|
|
setInFlightUrlProbe: (eventId, url, promise) => {
|
|
inFlightProbes.set(`${eventId}-${url}`, promise);
|
|
},
|
|
URL_PROBE_TIMEOUT_MS: 50,
|
|
urlHealthConstants: { URL_PROBE_TIMEOUT_RETRY_MS: 50 },
|
|
};
|
|
|
|
const mockLogger = {
|
|
warn: () => {},
|
|
log: () => {},
|
|
error: () => {},
|
|
};
|
|
|
|
const mockUtils = {
|
|
updateVideoCardSourceVisibility: () => {},
|
|
};
|
|
|
|
const mockCallbacks = {
|
|
getVideoListView: () => ({
|
|
cacheUrlHealth: () => {},
|
|
}),
|
|
};
|
|
|
|
beforeEach(() => {
|
|
dom = new JSDOM('<!DOCTYPE html><html><body></body></html>', {
|
|
url: 'https://example.com',
|
|
});
|
|
|
|
global.window = dom.window;
|
|
global.document = dom.window.document;
|
|
global.HTMLElement = dom.window.HTMLElement;
|
|
global.AbortController = dom.window.AbortController;
|
|
|
|
// Mock fetch
|
|
global.fetch = async (url) => {
|
|
if (url.includes('timeout')) {
|
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
return { ok: false, status: 408 };
|
|
}
|
|
if (url.includes('404')) {
|
|
return { ok: false, status: 404 };
|
|
}
|
|
if (url.includes('error')) {
|
|
throw new Error('Network error');
|
|
}
|
|
if (url.includes('opaque')) {
|
|
return { type: 'opaque', ok: false, status: 0 };
|
|
}
|
|
return { ok: true, status: 200, type: 'basic' };
|
|
};
|
|
|
|
mockCache.clear();
|
|
inFlightProbes.clear();
|
|
|
|
controller = new UrlHealthController({
|
|
state: mockState,
|
|
utils: mockUtils,
|
|
logger: mockLogger,
|
|
constants: { URL_PROBE_TIMEOUT_MS: 50 },
|
|
callbacks: mockCallbacks,
|
|
});
|
|
});
|
|
|
|
afterEach(() => {
|
|
delete global.window;
|
|
delete global.document;
|
|
delete global.HTMLElement;
|
|
delete global.fetch;
|
|
delete global.AbortController;
|
|
});
|
|
|
|
describe('UrlHealthController', () => {
|
|
it('probeUrl returns ok for valid URL', async () => {
|
|
// Mock video element behavior
|
|
const originalCreateElement = document.createElement;
|
|
document.createElement = (tagName) => {
|
|
if (tagName === 'video') {
|
|
return {
|
|
addEventListener: (event, handler) => {
|
|
if (event === 'loadeddata') {
|
|
setTimeout(handler, 10);
|
|
}
|
|
},
|
|
removeEventListener: () => {},
|
|
load: () => {},
|
|
pause: () => {},
|
|
removeAttribute: () => {},
|
|
isConnected: false,
|
|
};
|
|
}
|
|
return originalCreateElement(tagName);
|
|
};
|
|
|
|
const result = await controller.probeUrl('https://example.com/video.mp4', { confirmPlayable: true });
|
|
assert.equal(result.outcome, 'ok');
|
|
assert.equal(result.status, 200);
|
|
|
|
document.createElement = originalCreateElement;
|
|
});
|
|
|
|
it('probeUrl returns error for 404', async () => {
|
|
// Mock video element behavior to fail
|
|
const originalCreateElement = document.createElement;
|
|
document.createElement = (tagName) => {
|
|
if (tagName === 'video') {
|
|
return {
|
|
addEventListener: (event, handler) => {
|
|
if (event === 'error') {
|
|
setTimeout(handler, 10);
|
|
}
|
|
},
|
|
removeEventListener: () => {},
|
|
load: () => {},
|
|
pause: () => {},
|
|
removeAttribute: () => {},
|
|
isConnected: false,
|
|
};
|
|
}
|
|
return originalCreateElement(tagName);
|
|
};
|
|
|
|
const result = await controller.probeUrl('https://example.com/404.mp4', { confirmPlayable: true });
|
|
assert.ok(result.outcome === 'bad' || result.outcome === 'error');
|
|
|
|
document.createElement = originalCreateElement;
|
|
});
|
|
|
|
it('handleUrlHealthBadge updates badge', async () => {
|
|
const badgeEl = document.createElement('span');
|
|
document.body.appendChild(badgeEl); // Ensure isConnected is true
|
|
badgeEl.dataset.urlHealthUrl = 'https://example.com/video.mp4';
|
|
badgeEl.dataset.urlHealthEventId = 'event1';
|
|
|
|
// Mock successful probe via video element
|
|
const originalCreateElement = document.createElement;
|
|
document.createElement = (tagName) => {
|
|
if (tagName === 'video') {
|
|
return {
|
|
addEventListener: (event, handler) => {
|
|
if (event === 'loadeddata') {
|
|
setTimeout(handler, 0);
|
|
}
|
|
},
|
|
removeEventListener: () => {},
|
|
load: () => {},
|
|
pause: () => {},
|
|
removeAttribute: () => {},
|
|
isConnected: false,
|
|
};
|
|
}
|
|
return originalCreateElement(tagName);
|
|
};
|
|
|
|
await controller.handleUrlHealthBadge({
|
|
video: { id: 'event1' },
|
|
url: 'https://example.com/video.mp4',
|
|
badgeEl
|
|
});
|
|
|
|
// Since handleUrlHealthBadge sets an in-flight probe, we might need to wait or verify the state.
|
|
// The implementation of handleUrlHealthBadge awaits internally if not careful, or just triggers logic.
|
|
// In js/app.js, it sets the in-flight probe and attaches .then() handlers.
|
|
// We need to wait for microtasks.
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 50));
|
|
|
|
assert.equal(badgeEl.dataset.urlHealthState, 'healthy');
|
|
assert.equal(badgeEl.textContent, '✅ CDN');
|
|
|
|
document.createElement = originalCreateElement;
|
|
});
|
|
|
|
it('getUrlHealthPlaceholderMarkup returns string', () => {
|
|
const markup = controller.getUrlHealthPlaceholderMarkup();
|
|
assert.ok(typeof markup === 'string');
|
|
assert.ok(markup.includes('CDN'));
|
|
});
|
|
});
|