import test, { beforeEach, afterEach } from 'node:test'; import { strict as assert } from 'node:assert'; import { readFile } from 'node:fs/promises'; import { JSDOM } from 'jsdom'; import { EditModal } from '../js/ui/components/EditModal.js'; const editModalHtml = await readFile( new URL('../components/edit-video-modal.html', import.meta.url), 'utf8', ); let dom; let container; beforeEach(() => { dom = new JSDOM( '
', { url: 'https://example.com', pretendToBeVisual: true, }, ); global.window = dom.window; global.document = dom.window.document; global.HTMLElement = dom.window.HTMLElement; global.HTMLInputElement = dom.window.HTMLInputElement; global.HTMLButtonElement = dom.window.HTMLButtonElement; global.CustomEvent = dom.window.CustomEvent; global.Event = dom.window.Event; global.EventTarget = dom.window.EventTarget; container = document.getElementById('modalContainer'); global.fetch = async (resource) => { if ( typeof resource === 'string' && resource.includes('components/edit-video-modal.html') ) { return { ok: true, status: 200, text: async () => editModalHtml, }; } throw new Error(`Unexpected fetch: ${resource}`); }; }); afterEach(() => { delete global.fetch; delete global.EventTarget; delete global.Event; delete global.CustomEvent; delete global.HTMLButtonElement; delete global.HTMLInputElement; delete global.HTMLElement; delete global.document; delete global.window; if (dom) { dom.window.close(); dom = null; } container = null; }); function createModal({ showError, eventTarget = new EventTarget(), } = {}) { return new EditModal({ removeTrackingScripts: () => {}, setGlobalModalState: () => {}, showError: showError || (() => {}), getMode: () => 'live', eventTarget, container, }); } test('ignores additional submissions while pending without spurious errors', async () => { const errorMessages = []; const modalEvents = new EventTarget(); const modal = createModal({ showError: (message) => { if (message) { errorMessages.push(message); } }, eventTarget: modalEvents, }); await modal.load({ container }); const video = { id: 'video1', pubkey: 'pubkey1', title: 'Sample video', url: 'https://example.com/video.mp4', thumbnail: '', description: '', enableComments: true, isPrivate: false, isNsfw: true, isForKids: false, }; await modal.open(video); assert.ok(modal.submitButton, 'expected submit button to be cached'); assert.equal(modal.submitButton.disabled, false); const submissions = []; modal.addEventListener('video:edit-submit', (event) => { submissions.push(event.detail); }); modal.submit(); assert.equal(submissions.length, 1, 'first submit should emit once'); assert.equal(modal.pendingSubmit, true, 'pending flag should be set after submit'); assert.equal( modal.pendingSubmitVideo?.id, video.id, 'pending video context should match the edited video', ); assert.equal(modal.submitButton.disabled, true, 'button should disable while pending'); const firstSubmission = submissions[0]; assert.ok(firstSubmission, 'expected first submission payload'); assert.equal( firstSubmission.updatedData.isNsfw, true, 'locked NSFW flag should persist in emitted payload', ); assert.equal( firstSubmission.updatedData.isForKids, false, 'locked kids flag should persist in emitted payload', ); modal.submit(); assert.equal(submissions.length, 1, 'duplicate submit should be ignored'); assert.equal( errorMessages.includes('No video selected for editing.'), false, 'should not surface missing video error during pending submit', ); modal.setSubmitState({ pending: false }); assert.equal(modal.pendingSubmit, false, 'pending flag should reset after release'); assert.equal(modal.submitButton.disabled, false, 'button should re-enable after release'); const unlockButton = (selector) => modal.root.querySelector(selector)?.dispatchEvent( new dom.window.Event('click', { bubbles: true }), ); unlockButton('[data-edit-target="editVideoIsNsfw"]'); unlockButton('[data-edit-target="editVideoIsForKids"]'); const nsfwCheckbox = modal.fields.isNsfw; const kidsCheckbox = modal.fields.isForKids; assert.ok(nsfwCheckbox, 'expected nsfw checkbox to be cached'); assert.ok(kidsCheckbox, 'expected kids checkbox to be cached'); nsfwCheckbox.checked = false; kidsCheckbox.checked = true; modal.submit(); assert.equal(submissions.length, 2, 'unlocked submit should emit again'); const secondSubmission = submissions[1]; assert.ok(secondSubmission, 'expected second submission payload'); assert.equal( secondSubmission.updatedData.isNsfw, false, 'updated NSFW flag should reflect unlocked checkbox state', ); assert.equal( secondSubmission.updatedData.isForKids, true, 'updated kids flag should reflect unlocked checkbox state', ); modal.setSubmitState({ pending: false }); }); test('editing the magnet refreshes torrent hints when ws/xs remain locked', async () => { const modal = createModal(); await modal.load({ container }); const originalMagnet = 'magnet:?xt=urn:btih:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&ws=https://old.example/video.mp4&xs=https://old.example/video.torrent'; const video = { id: 'video-magnet-locked', pubkey: 'pubkey-magnet-locked', title: 'Locked hints example', url: 'https://example.com/locked.mp4', magnet: originalMagnet, ws: 'https://old.example/video.mp4', xs: 'https://old.example/video.torrent', }; await modal.open(video); const submissions = []; modal.addEventListener('video:edit-submit', (event) => { submissions.push(event.detail); }); const magnetButton = modal.root.querySelector( '[data-edit-target="editVideoMagnet"]', ); assert.ok(magnetButton, 'expected magnet unlock button to exist'); magnetButton.dispatchEvent(new dom.window.Event('click', { bubbles: true })); const magnetInput = modal.fields.magnet; assert.ok(magnetInput, 'expected magnet input to exist'); magnetInput.value = 'magnet:?xt=urn:btih:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb&ws=https://new.example/video.mp4&xs=https://new.example/video.torrent'; modal.submit(); assert.equal(submissions.length, 1, 'should emit a single submission payload'); const submission = submissions[0]; assert.ok(submission, 'expected edit submission detail'); assert.equal( submission.updatedData.wsEdited, true, 'ws should be marked as edited when magnet changes', ); assert.equal( submission.updatedData.xsEdited, true, 'xs should be marked as edited when magnet changes', ); assert.equal( submission.updatedData.ws, 'https://new.example/video.mp4', 'ws hint should refresh to the new magnet hint', ); assert.equal( submission.updatedData.xs, 'https://new.example/video.torrent', 'xs hint should refresh to the new magnet hint', ); assert.ok( submission.updatedData.magnet.includes( 'urn%3Abtih%3Abbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', ), 'normalized magnet should contain the updated info hash (encoded)', ); assert.ok( submission.updatedData.magnet.includes('ws=https%3A%2F%2Fnew.example%2Fvideo.mp4'), 'normalized magnet should include the refreshed ws hint (encoded)', ); assert.ok( submission.updatedData.magnet.includes( 'xs=https%3A%2F%2Fnew.example%2Fvideo.torrent', ), 'normalized magnet should include the refreshed xs hint (encoded)', ); modal.setSubmitState({ pending: false }); }); test('does not show missing video error after modal closes', async () => { const errorMessages = []; const modal = createModal({ showError: (message) => { if (message) { errorMessages.push(message); } }, }); await modal.load({ container }); const video = { id: 'video2', pubkey: 'pubkey2', title: 'Another video', url: 'https://example.com/another.mp4', }; await modal.open(video); modal.submit(); assert.equal(errorMessages.includes('No video selected for editing.'), false); modal.setSubmitState({ pending: false }); modal.close(); assert.equal(modal.isVisible, false, 'modal should be hidden after close'); modal.submit(); assert.equal( errorMessages.includes('No video selected for editing.'), false, 'should not surface missing video error when closed', ); });