mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 16:02:26 +00:00
WebTorrent (handleTorrentStream) hardening:
- Resolve only when playback is actually viable (loadeddata/canplay/playing),
not the instant streamTo() is called. A dead/too-slow swarm is no longer
reported as success, so the caller's fallback can run.
- Pre-playback torrent errors reject (caller falls back); post-playback errors
go to an onPlaybackError hook instead of a dead reject().
- Mid-stream stall watchdog surfaces an onStall hook ("waiting for peers").
- Fix a <video> listener leak: stream listeners are tracked and torn down
before the next stream and on cleanup() (the modal reuses one element).
- Stream the largest decodable file from a multi-file torrent and deselect the
rest (wider ext set: mp4/m4v/webm/mkv/mov/ogv/ogg) so the swarm isn't spent
downloading files we never play.
playbackCoordinator wires onStall/onPlaybackError to user-facing status.
Forced-source deep links (test affordance):
- The main app honors ?v=<nevent>&playback=torrent|url (mirrors the embed
player) to force a playback path; a missing source shows a clear message.
- The video share menu gains "Copy WebTorrent URL" / "Copy CDN URL" items that
copy the forced-source deep link (disabled with a reason when unavailable).
Tests: webtorrent-handlers (resolve-on-readiness, pre/post-ready errors, stall
fires/clears, largest-file select/deselect, no listener accumulation),
playback-forced-source (param normalization), videoMenuRenderers (copy items +
availability). All mutation-verified.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
291 lines
9.1 KiB
JavaScript
291 lines
9.1 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { JSDOM } from 'jsdom';
|
|
import {
|
|
createVideoMoreMenuPanel,
|
|
createVideoShareMenuPanel,
|
|
createChannelProfileMenuPanel,
|
|
createVideoSettingsMenuPanel,
|
|
} from '../../../js/ui/components/videoMenuRenderers.js';
|
|
|
|
function setupDom() {
|
|
const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>', {
|
|
url: 'https://example.com',
|
|
});
|
|
return { window: dom.window, document: dom.window.document };
|
|
}
|
|
|
|
test('createVideoMoreMenuPanel - basic rendering', () => {
|
|
const { document } = setupDom();
|
|
const video = {
|
|
id: 'event-123',
|
|
pubkey: 'pubkey-123',
|
|
title: 'Test Video',
|
|
description: 'Test Description',
|
|
thumbnail: 'thumbnail.jpg',
|
|
isPrivate: false,
|
|
kind: 30078,
|
|
};
|
|
|
|
const panel = createVideoMoreMenuPanel({
|
|
document,
|
|
video,
|
|
context: 'card',
|
|
});
|
|
|
|
assert.ok(panel, 'Panel should be created');
|
|
assert.equal(panel.dataset.menu, 'video-more');
|
|
assert.equal(panel.dataset.menuContext, 'card');
|
|
|
|
// Check common actions
|
|
const openChannel = panel.querySelector('[data-action="open-channel"]');
|
|
assert.ok(openChannel, 'Open channel button exists');
|
|
assert.equal(openChannel.dataset.author, video.pubkey);
|
|
|
|
const copyLink = panel.querySelector('[data-action="copy-link"]');
|
|
assert.ok(copyLink, 'Copy link button exists');
|
|
assert.equal(copyLink.dataset.eventId, video.id);
|
|
|
|
// Check Boost heading
|
|
const headings = Array.from(panel.querySelectorAll('.menu__heading'));
|
|
assert.ok(headings.some(h => h.textContent === 'Boost on Nostr…'), 'Boost heading exists');
|
|
|
|
// Check Repost
|
|
const repost = panel.querySelector('[data-action="repost-event"]');
|
|
assert.ok(repost, 'Repost button exists');
|
|
assert.equal(repost.dataset.eventId, video.id);
|
|
|
|
// Check Mute/Block/Report
|
|
assert.ok(panel.querySelector('[data-action="mute-author"]'), 'Mute button exists');
|
|
assert.ok(panel.querySelector('[data-action="block-author"]'), 'Block button exists');
|
|
assert.ok(panel.querySelector('[data-action="report"]'), 'Report button exists');
|
|
});
|
|
|
|
test('createVideoMoreMenuPanel - pointer info rendering', () => {
|
|
const { document } = setupDom();
|
|
const video = { id: 'v1', pubkey: 'p1' };
|
|
const pointerInfo = {
|
|
key: 'history-key',
|
|
pointer: ['a', 'value', 'relay'],
|
|
};
|
|
|
|
const panel = createVideoMoreMenuPanel({
|
|
document,
|
|
video,
|
|
pointerInfo,
|
|
});
|
|
|
|
const removeHistory = panel.querySelector('[data-action="remove-history"]');
|
|
assert.ok(removeHistory, 'Remove from history button exists');
|
|
assert.equal(removeHistory.dataset.pointerKey, 'history-key');
|
|
assert.equal(removeHistory.dataset.pointerValue, 'value');
|
|
assert.equal(removeHistory.dataset.pointerRelay, 'relay');
|
|
});
|
|
|
|
test('createVideoMoreMenuPanel - mirror logic', () => {
|
|
const { document } = setupDom();
|
|
const video = { id: 'v1', isPrivate: false };
|
|
const playbackUrl = 'https://example.com/video.mp4';
|
|
const playbackMagnet = 'magnet:?xt=urn:btih:123';
|
|
|
|
const panel = createVideoMoreMenuPanel({
|
|
document,
|
|
video,
|
|
playbackUrl,
|
|
playbackMagnet,
|
|
});
|
|
|
|
const mirror = panel.querySelector('[data-action="mirror-video"]');
|
|
assert.ok(mirror, 'Mirror button exists');
|
|
assert.equal(mirror.dataset.url, playbackUrl);
|
|
assert.equal(mirror.dataset.magnet, playbackMagnet);
|
|
assert.equal(mirror.dataset.isPrivate, 'false');
|
|
});
|
|
|
|
test('createVideoMoreMenuPanel - blacklist logic', () => {
|
|
const { document } = setupDom();
|
|
const video = { id: 'v1', pubkey: 'p1' };
|
|
|
|
const panel = createVideoMoreMenuPanel({
|
|
document,
|
|
video,
|
|
canManageBlacklist: true,
|
|
});
|
|
|
|
const blacklist = panel.querySelector('[data-action="blacklist-author"]');
|
|
assert.ok(blacklist, 'Blacklist button exists');
|
|
assert.equal(blacklist.dataset.variant, 'critical');
|
|
});
|
|
|
|
test('createVideoShareMenuPanel - permission logic', () => {
|
|
const { document } = setupDom();
|
|
const video = { id: 'v1' };
|
|
|
|
// Case 1: Not logged in
|
|
const panel1 = createVideoShareMenuPanel({
|
|
document,
|
|
video,
|
|
isLoggedIn: false,
|
|
hasSigner: false,
|
|
});
|
|
const nostrBtn1 = panel1.querySelector('[data-action="share-nostr"]');
|
|
assert.ok(nostrBtn1.disabled, 'Share on Nostr should be disabled when not logged in');
|
|
|
|
// Case 2: Logged in but no signer
|
|
const panel2 = createVideoShareMenuPanel({
|
|
document,
|
|
video,
|
|
isLoggedIn: true,
|
|
hasSigner: false,
|
|
});
|
|
const nostrBtn2 = panel2.querySelector('[data-action="share-nostr"]');
|
|
assert.ok(nostrBtn2.disabled, 'Share on Nostr should be disabled without signer');
|
|
|
|
// Case 3: Logged in with signer
|
|
const panel3 = createVideoShareMenuPanel({
|
|
document,
|
|
video,
|
|
isLoggedIn: true,
|
|
hasSigner: true,
|
|
});
|
|
const nostrBtn3 = panel3.querySelector('[data-action="share-nostr"]');
|
|
assert.ok(!nostrBtn3.disabled, 'Share on Nostr should be enabled with signer');
|
|
});
|
|
|
|
test('createVideoShareMenuPanel - magnet/cdn logic', () => {
|
|
const { document } = setupDom();
|
|
const video = { id: 'v1' };
|
|
|
|
const panel = createVideoShareMenuPanel({
|
|
document,
|
|
video,
|
|
hasMagnet: true,
|
|
hasCdn: false,
|
|
});
|
|
|
|
const magnetBtn = panel.querySelector('[data-action="copy-magnet"]');
|
|
assert.ok(!magnetBtn.disabled, 'Magnet button should be enabled');
|
|
|
|
const cdnBtn = panel.querySelector('[data-action="copy-cdn"]');
|
|
assert.ok(cdnBtn.disabled, 'CDN button should be disabled');
|
|
});
|
|
|
|
test('createVideoShareMenuPanel - forced-source deep links copy via builder', async () => {
|
|
const { document } = setupDom();
|
|
const video = { id: 'v1' };
|
|
|
|
const built = [];
|
|
const copied = [];
|
|
const buildForcedSourceUrl = (playback) => {
|
|
built.push(playback);
|
|
return `https://bitvid.test/?v=nevent1&playback=${playback}`;
|
|
};
|
|
const clipboard = {
|
|
writeText: async (text) => {
|
|
copied.push(text);
|
|
},
|
|
};
|
|
|
|
const panel = createVideoShareMenuPanel({
|
|
document,
|
|
video,
|
|
hasMagnet: true,
|
|
hasCdn: true,
|
|
buildForcedSourceUrl,
|
|
clipboard,
|
|
});
|
|
|
|
const torrentBtn = panel.querySelector('[data-action="copy-webtorrent-url"]');
|
|
const cdnBtn = panel.querySelector('[data-action="copy-cdn-url"]');
|
|
assert.ok(torrentBtn, 'WebTorrent URL item should exist');
|
|
assert.ok(cdnBtn, 'CDN URL item should exist');
|
|
assert.ok(!torrentBtn.disabled, 'WebTorrent URL enabled when a magnet exists');
|
|
assert.ok(!cdnBtn.disabled, 'CDN URL enabled when a CDN url exists');
|
|
|
|
torrentBtn.click();
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
|
|
assert.deepEqual(built, ['torrent'], 'builder asked for the torrent source');
|
|
assert.deepEqual(
|
|
copied,
|
|
['https://bitvid.test/?v=nevent1&playback=torrent'],
|
|
'the forced-torrent deep link is copied to the clipboard',
|
|
);
|
|
});
|
|
|
|
test('createVideoShareMenuPanel - forced-source items disabled (with message) when source missing', () => {
|
|
const { document } = setupDom();
|
|
|
|
const panel = createVideoShareMenuPanel({
|
|
document,
|
|
video: { id: 'v1' },
|
|
hasMagnet: false,
|
|
hasCdn: true,
|
|
buildForcedSourceUrl: () => 'https://bitvid.test/?v=nevent1&playback=torrent',
|
|
});
|
|
|
|
const torrentBtn = panel.querySelector('[data-action="copy-webtorrent-url"]');
|
|
assert.ok(torrentBtn.disabled, 'WebTorrent URL disabled when no magnet');
|
|
assert.match(
|
|
torrentBtn.title,
|
|
/no webtorrent source/i,
|
|
'disabled WebTorrent URL explains why',
|
|
);
|
|
|
|
const cdnBtn = panel.querySelector('[data-action="copy-cdn-url"]');
|
|
assert.ok(!cdnBtn.disabled, 'CDN URL enabled when a CDN url exists');
|
|
});
|
|
|
|
test('createVideoShareMenuPanel - no forced-source items without a builder', () => {
|
|
const { document } = setupDom();
|
|
const panel = createVideoShareMenuPanel({
|
|
document,
|
|
video: { id: 'v1' },
|
|
hasMagnet: true,
|
|
hasCdn: true,
|
|
});
|
|
assert.equal(
|
|
panel.querySelector('[data-action="copy-webtorrent-url"]'),
|
|
null,
|
|
'no WebTorrent URL item when no builder is provided',
|
|
);
|
|
});
|
|
|
|
test('createChannelProfileMenuPanel - basic actions', () => {
|
|
const { document } = setupDom();
|
|
const panel = createChannelProfileMenuPanel({
|
|
document,
|
|
context: 'profile',
|
|
});
|
|
|
|
assert.equal(panel.dataset.menu, 'channel-profile');
|
|
assert.ok(panel.querySelector('[data-action="copy-npub"]'), 'Copy npub exists');
|
|
assert.ok(panel.querySelector('[data-action="mute-author"]'), 'Mute author exists');
|
|
assert.ok(panel.querySelector('[data-action="report"]'), 'Report exists');
|
|
});
|
|
|
|
test('createVideoSettingsMenuPanel - capabilities', () => {
|
|
const { document } = setupDom();
|
|
const video = { id: 'v1' };
|
|
|
|
// Case 1: Edit only
|
|
const panel1 = createVideoSettingsMenuPanel({
|
|
document,
|
|
video,
|
|
capabilities: { canEdit: true },
|
|
});
|
|
assert.ok(panel1.querySelector('[data-action="edit"]'), 'Edit button exists');
|
|
assert.ok(!panel1.querySelector('[data-action="revert"]'), 'Revert button absent');
|
|
assert.ok(!panel1.querySelector('[data-action="delete"]'), 'Delete button absent');
|
|
|
|
// Case 2: Full capabilities
|
|
const panel2 = createVideoSettingsMenuPanel({
|
|
document,
|
|
video,
|
|
capabilities: { canEdit: true, canRevert: true, canDelete: true },
|
|
});
|
|
assert.ok(panel2.querySelector('[data-action="revert"]'), 'Revert button exists');
|
|
assert.ok(panel2.querySelector('[data-action="delete"]'), 'Delete button exists');
|
|
});
|