import { test, describe, it, before, after } 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 {}; } }; 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; }); after(() => { if (dom) dom.cleanup(); }); const mockHtml = ` `; it("should reset upload state and inputs when resetUploads is called", async () => { const container = document.createElement("div"); container.id = "modalContainer"; document.body.appendChild(container); global.fetch = async () => ({ ok: true, text: async () => mockHtml }); 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 slowR2Service = { prepareUpload: async () => ({ settings: {}, bucketEntry: { bucket: 'test', publicBaseUrl: 'https://test.com' } }), uploadFile: () => delayedUpload // Return promise that waits }; const modal = new UploadModal({ storageService: mockStorageService, r2Service: slowR2Service, 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' }; // Mock generateTorrentMetadata BEFORE calling handleVideoSelection modal.generateTorrentMetadata = async () => ({ hasValidInfoHash: false }); modal.resolveUploadIdentifier = async () => "mock-identifier"; // 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"); }); });