import { test, describe, it, before, after, beforeEach, afterEach } from "node:test"; import assert from "node:assert"; import { createUiDom } from "./helpers/jsdom-test-helpers.mjs"; describe("UploadModal Reset Logic", () => { let dom; let UploadModal; // Mock StorageService const mockStorageService = { isUnlocked: () => true, unlock: async () => {}, listConnections: async () => [], getConnection: async () => null, saveConnection: async () => {}, isMock: true }; // Mock R2Service const mockR2Service = { prepareUpload: async () => ({ settings: {}, bucketEntry: { bucket: 'test', publicBaseUrl: 'https://test.com' } }), uploadFile: async () => { // Default immediate resolution return {}; } }; const mockHtml = ` `; before(async () => { process.env.NODE_ENV = 'test'; dom = createUiDom(); global.window = dom.window; global.self = dom.window; global.document = dom.document; global.EventTarget = dom.window.EventTarget; global.alert = (msg) => { console.log("ALERT CALLED:", msg); }; global.HTMLElement = dom.window.HTMLElement; global.HTMLInputElement = dom.window.HTMLInputElement; global.HTMLButtonElement = dom.window.HTMLButtonElement; global.HTMLDivElement = dom.window.HTMLDivElement; const module = await import("../../js/ui/components/UploadModal.js"); UploadModal = module.UploadModal; }); beforeEach(() => { global.fetch = async () => ({ ok: true, text: async () => mockHtml }); }); afterEach(() => { document.body.innerHTML = ""; }); after(() => { if (dom) dom.cleanup(); }); it("should reset upload state and inputs when resetUploads is called", async () => { const container = document.createElement("div"); container.id = "modalContainer"; document.body.appendChild(container); const modal = new UploadModal({ storageService: mockStorageService, r2Service: mockR2Service, getCurrentPubkey: () => "pk1", removeTrackingScripts: () => {}, setGlobalModalState: () => {}, }); await modal.load({ container }); // Simulate modified state modal.videoUploadState.status = 'complete'; modal.inputs.file = { value: 'fake-path' }; // Mock input value modal.inputs.thumbnail.disabled = true; modal.inputs.thumbnail.placeholder = "Selected: file.jpg"; const initialVideoId = modal.videoUploadId; modal.resetUploads(); // Assertions assert.strictEqual(modal.videoUploadState.status, 'idle', "State should be idle"); assert.strictEqual(modal.inputs.file.value, "", "File input should be cleared"); assert.strictEqual(modal.inputs.thumbnail.disabled, false, "Thumbnail input should be enabled"); assert.strictEqual(modal.inputs.thumbnail.placeholder, "https://example.com/thumbnail.jpg", "Thumbnail placeholder should be reset"); assert.strictEqual(modal.videoUploadId, initialVideoId + 1, "Video upload ID should be incremented"); }); it("should guard against zombie callbacks when upload is reset during process", async () => { const container = document.createElement("div"); container.id = "modalContainer2"; document.body.appendChild(container); let resolveUpload; const delayedUpload = new Promise(r => { resolveUpload = r; }); const modal = new UploadModal({ storageService: mockStorageService, r2Service: mockR2Service, getCurrentPubkey: () => "pk1", removeTrackingScripts: () => {}, setGlobalModalState: () => {}, }); await modal.load({ container }); // Force configured state modal.storageConfigured = true; modal.isStorageUnlocked = true; modal.activeProvider = "cloudflare_r2"; modal.activeCredentials = { accountId: 'acc', accessKeyId: 'key', secretAccessKey: 'sec' }; // handleVideoSelection() drives the upload through this.mediaUploader.uploadVideo // — NOT the modal's own generate/resolve helpers — so mock at THAT boundary. // Returning the controllable pending promise lets us reset mid-upload and then // release a "zombie" completion. This isolates the unit under test (the modal's // videoUploadId versioning guard) from real webtorrent/S3, which is what the // test intends. Previously the test mocked modal.generateTorrentMetadata / // resolveUploadIdentifier (the wrong object), so the mediaUploader still ran // real createTorrentMetadata()/hashing on a synthetic file — throwing "invalid // input type" as async activity after the test ended. modal.mediaUploader.uploadVideo = () => delayedUpload; // captureVideoMetadata() does URL.createObjectURL(file), which needs a real // Blob; stub it so the synthetic file below doesn't throw an async Blob error. modal.captureVideoMetadata = async () => {}; // Start upload const file = { name: "video.mp4" }; const event = { target: { files: [file], value: "video.mp4" } }; // Start the async process without awaiting it yet const uploadPromise = modal.handleVideoSelection(event); // Verify it started assert.strictEqual(modal.videoUploadState.status, 'uploading'); // RESET immediately modal.resetUploads(); // Verify it reset assert.strictEqual(modal.videoUploadState.status, 'idle'); // Now let the upload promise resolve resolveUpload({ bucket: 'b', key: 'k' }); await uploadPromise; // Assert that state remains 'idle' and wasn't overwritten by 'complete' assert.strictEqual(modal.videoUploadState.status, 'idle', "State should remain idle after zombie completion"); assert.strictEqual(modal.results.videoUrl.value, "", "Result URL should be empty"); }); it("should call resetUploads when close is called", async () => { const container = document.createElement("div"); container.id = "modalContainer3"; document.body.appendChild(container); const modal = new UploadModal({ storageService: mockStorageService, r2Service: mockR2Service, setGlobalModalState: () => {}, }); await modal.load({ container }); let resetCalled = false; const originalReset = modal.resetUploads.bind(modal); modal.resetUploads = () => { resetCalled = true; originalReset(); }; modal.close(); assert.strictEqual(resetCalled, true, "resetUploads should be called on close"); }); });