Files
bitvid/tests/ui/components/videoMenuRenderers.test.mjs
T
thePR0M3TH3ANandClaude Opus 4.8 4e5bfaa6af [playback] harden WebTorrent streaming + add forced-source test deep links
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]>
2026-06-18 12:15:27 -04:00

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');
});