Framework-Specific Guides15 min readSeptember 18, 2025

Testing File Uploads in React Applications

Testing File Uploads in React Applications

Testing file upload functionality in React applications presents unique challenges, from mocking File objects to testing drag-and-drop interactions. This guide provides comprehensive testing strategies using React Testing Library and Jest.

Learn proven patterns for testing file upload components, handling async operations, and ensuring robust user experiences across different scenarios.

Setting Up React File Upload Testing

Essential Testing Dependencies

npm install --save-dev @testing-library/react @testing-library/jest-dom
npm install --save-dev @testing-library/user-event jest-environment-jsdom

Basic Test Environment Configuration

// setupTests.js
import "@testing-library/jest-dom";

// Mock URL.createObjectURL for file preview testing
global.URL.createObjectURL = jest.fn(() => "mock-url");
global.URL.revokeObjectURL = jest.fn();

// Mock File API
global.File = class MockFile {
  constructor(parts, filename, properties = {}) {
    this.parts = parts;
    this.name = filename;
    this.size = parts.reduce((acc, part) => acc + part.length, 0);
    this.type = properties.type || "";
    this.lastModified = properties.lastModified || Date.now();
  }
};

// Mock FileReader for file content testing
global.FileReader = class MockFileReader {
  constructor() {
    this.readyState = 0;
    this.result = null;
    this.error = null;
  }

  readAsDataURL(file) {
    setTimeout(() => {
      this.readyState = 2;
      this.result = `data:${file.type};base64,mock-base64-content`;
      this.onload && this.onload();
    }, 10);
  }

  readAsText(file) {
    setTimeout(() => {
      this.readyState = 2;
      this.result = "mock file content";
      this.onload && this.onload();
    }, 10);
  }
};

Testing Basic File Upload Components

Simple File Input Testing

// FileUpload.jsx
import React, { useState } from "react";

const FileUpload = ({ onFileSelect, accept, maxSize, multiple = false }) => {
  const [error, setError] = useState("");
  const [uploading, setUploading] = useState(false);

  const handleFileChange = (event) => {
    const files = Array.from(event.target.files);
    setError("");

    // Validate file size
    const oversizedFiles = files.filter((file) => file.size > maxSize);
    if (oversizedFiles.length > 0) {
      setError(`File size exceeds ${maxSize / 1024 / 1024}MB limit`);
      return;
    }

    // Validate file type
    if (accept) {
      const acceptedTypes = accept.split(",").map((type) => type.trim());
      const invalidFiles = files.filter((file) => {
        return !acceptedTypes.some((type) => {
          if (type.startsWith(".")) {
            return file.name.toLowerCase().endsWith(type.toLowerCase());
          }
          return file.type.match(type.replace("*", ".*"));
        });
      });

      if (invalidFiles.length > 0) {
        setError("Invalid file type selected");
        return;
      }
    }

    setUploading(true);
    onFileSelect(multiple ? files : files[0]);
    setUploading(false);
  };

  return (
    <div className="file-upload">
      <input
        type="file"
        onChange={handleFileChange}
        accept={accept}
        multiple={multiple}
        disabled={uploading}
        data-testid="file-input"
      />
      {error && (
        <div className="error" data-testid="error-message">
          {error}
        </div>
      )}
      {uploading && (
        <div className="uploading" data-testid="uploading-indicator">
          Uploading...
        </div>
      )}
    </div>
  );
};

export default FileUpload;

Comprehensive Component Tests

// FileUpload.test.jsx
import React from "react";
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import FileUpload from "./FileUpload";

// Helper function to create mock files
const createMockFile = (name, size, type) => {
  const file = new File(["test content"], name, { type });
  Object.defineProperty(file, "size", { value: size });
  return file;
};

describe("FileUpload Component", () => {
  const mockOnFileSelect = jest.fn();
  const defaultProps = {
    onFileSelect: mockOnFileSelect,
    accept: "image/*",
    maxSize: 5 * 1024 * 1024, // 5MB
  };

  beforeEach(() => {
    mockOnFileSelect.mockClear();
  });

  test("renders file input correctly", () => {
    render(<FileUpload {...defaultProps} />);

    const fileInput = screen.getByTestId("file-input");
    expect(fileInput).toBeInTheDocument();
    expect(fileInput).toHaveAttribute("accept", "image/*");
    expect(fileInput).not.toHaveAttribute("multiple");
  });

  test("enables multiple file selection when multiple prop is true", () => {
    render(<FileUpload {...defaultProps} multiple />);

    const fileInput = screen.getByTestId("file-input");
    expect(fileInput).toHaveAttribute("multiple");
  });

  test("handles valid file selection", async () => {
    const user = userEvent.setup();
    render(<FileUpload {...defaultProps} />);

    const fileInput = screen.getByTestId("file-input");
    const validFile = createMockFile("test.jpg", 1024 * 1024, "image/jpeg");

    await user.upload(fileInput, validFile);

    expect(mockOnFileSelect).toHaveBeenCalledWith(validFile);
  });

  test("handles multiple file selection", async () => {
    const user = userEvent.setup();
    render(<FileUpload {...defaultProps} multiple />);

    const fileInput = screen.getByTestId("file-input");
    const files = [createMockFile("test1.jpg", 1024, "image/jpeg"), createMockFile("test2.png", 2048, "image/png")];

    await user.upload(fileInput, files);

    expect(mockOnFileSelect).toHaveBeenCalledWith(files);
  });

  test("shows error for oversized files", async () => {
    const user = userEvent.setup();
    render(<FileUpload {...defaultProps} />);

    const fileInput = screen.getByTestId("file-input");
    const oversizedFile = createMockFile("large.jpg", 10 * 1024 * 1024, "image/jpeg");

    await user.upload(fileInput, oversizedFile);

    expect(screen.getByTestId("error-message")).toHaveTextContent("File size exceeds 5MB limit");
    expect(mockOnFileSelect).not.toHaveBeenCalled();
  });

  test("shows error for invalid file types", async () => {
    const user = userEvent.setup();
    render(<FileUpload {...defaultProps} />);

    const fileInput = screen.getByTestId("file-input");
    const invalidFile = createMockFile("document.pdf", 1024, "application/pdf");

    await user.upload(fileInput, invalidFile);

    expect(screen.getByTestId("error-message")).toHaveTextContent("Invalid file type selected");
    expect(mockOnFileSelect).not.toHaveBeenCalled();
  });

  test("validates file extensions when MIME type is not available", async () => {
    const user = userEvent.setup();
    render(<FileUpload {...defaultProps} accept=".jpg,.png" />);

    const fileInput = screen.getByTestId("file-input");
    const validFile = createMockFile("test.jpg", 1024, "");

    await user.upload(fileInput, validFile);

    expect(mockOnFileSelect).toHaveBeenCalledWith(validFile);
  });

  test("clears previous errors on valid file selection", async () => {
    const user = userEvent.setup();
    render(<FileUpload {...defaultProps} />);

    const fileInput = screen.getByTestId("file-input");

    // First, trigger an error
    const invalidFile = createMockFile("document.pdf", 1024, "application/pdf");
    await user.upload(fileInput, invalidFile);
    expect(screen.getByTestId("error-message")).toBeInTheDocument();

    // Then, select a valid file
    const validFile = createMockFile("test.jpg", 1024, "image/jpeg");
    await user.upload(fileInput, validFile);

    expect(screen.queryByTestId("error-message")).not.toBeInTheDocument();
    expect(mockOnFileSelect).toHaveBeenCalledWith(validFile);
  });
});

Testing Drag and Drop File Upload

Drag and Drop Component

// DragDropUpload.jsx
import React, { useState, useCallback } from "react";

const DragDropUpload = ({ onFileDrop, accept, maxFiles = 5 }) => {
  const [isDragOver, setIsDragOver] = useState(false);
  const [uploadedFiles, setUploadedFiles] = useState([]);

  const handleDragOver = useCallback((e) => {
    e.preventDefault();
    setIsDragOver(true);
  }, []);

  const handleDragLeave = useCallback((e) => {
    e.preventDefault();
    setIsDragOver(false);
  }, []);

  const handleDrop = useCallback(
    (e) => {
      e.preventDefault();
      setIsDragOver(false);

      const files = Array.from(e.dataTransfer.files);

      if (files.length + uploadedFiles.length > maxFiles) {
        alert(`Maximum ${maxFiles} files allowed`);
        return;
      }

      setUploadedFiles((prev) => [...prev, ...files]);
      onFileDrop(files);
    },
    [onFileDrop, maxFiles, uploadedFiles.length]
  );

  const removeFile = (index) => {
    setUploadedFiles((prev) => prev.filter((_, i) => i !== index));
  };

  return (
    <div className="drag-drop-container">
      <div
        className={`drop-zone ${isDragOver ? "drag-over" : ""}`}
        onDragOver={handleDragOver}
        onDragLeave={handleDragLeave}
        onDrop={handleDrop}
        data-testid="drop-zone"
      >
        {isDragOver ? (
          <p data-testid="drop-indicator">Drop files here</p>
        ) : (
          <p data-testid="upload-instruction">Drag and drop files here or click to browse</p>
        )}
      </div>

      {uploadedFiles.length > 0 && (
        <div className="file-list" data-testid="file-list">
          {uploadedFiles.map((file, index) => (
            <div key={index} className="file-item" data-testid="file-item">
              <span>{file.name}</span>
              <button onClick={() => removeFile(index)} data-testid={`remove-file-${index}`}>
                Remove
              </button>
            </div>
          ))}
        </div>
      )}
    </div>
  );
};

export default DragDropUpload;

Drag and Drop Testing

// DragDropUpload.test.jsx
import React from "react";
import { render, screen, fireEvent } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import DragDropUpload from "./DragDropUpload";

// Helper to create DataTransfer object for drag events
const createDataTransfer = (files) => ({
  dataTransfer: {
    files: {
      ...files,
      length: files.length,
      [Symbol.iterator]: function* () {
        for (let i = 0; i < files.length; i++) {
          yield files[i];
        }
      },
    },
  },
});

describe("DragDropUpload Component", () => {
  const mockOnFileDrop = jest.fn();
  const defaultProps = {
    onFileDrop: mockOnFileDrop,
    accept: "image/*",
    maxFiles: 3,
  };

  beforeEach(() => {
    mockOnFileDrop.mockClear();
    // Mock alert for testing
    window.alert = jest.fn();
  });

  afterEach(() => {
    jest.restoreAllMocks();
  });

  test("renders drop zone correctly", () => {
    render(<DragDropUpload {...defaultProps} />);

    expect(screen.getByTestId("drop-zone")).toBeInTheDocument();
    expect(screen.getByTestId("upload-instruction")).toHaveTextContent("Drag and drop files here or click to browse");
  });

  test("shows drag over state during drag operation", () => {
    render(<DragDropUpload {...defaultProps} />);

    const dropZone = screen.getByTestId("drop-zone");

    fireEvent.dragOver(dropZone);

    expect(dropZone).toHaveClass("drag-over");
    expect(screen.getByTestId("drop-indicator")).toHaveTextContent("Drop files here");
  });

  test("removes drag over state on drag leave", () => {
    render(<DragDropUpload {...defaultProps} />);

    const dropZone = screen.getByTestId("drop-zone");

    fireEvent.dragOver(dropZone);
    expect(dropZone).toHaveClass("drag-over");

    fireEvent.dragLeave(dropZone);
    expect(dropZone).not.toHaveClass("drag-over");
  });

  test("handles file drop correctly", () => {
    render(<DragDropUpload {...defaultProps} />);

    const dropZone = screen.getByTestId("drop-zone");
    const files = [
      new File(["content1"], "file1.jpg", { type: "image/jpeg" }),
      new File(["content2"], "file2.png", { type: "image/png" }),
    ];

    fireEvent.drop(dropZone, createDataTransfer(files));

    expect(mockOnFileDrop).toHaveBeenCalledWith(files);
    expect(screen.getByTestId("file-list")).toBeInTheDocument();
    expect(screen.getAllByTestId("file-item")).toHaveLength(2);
  });

  test("displays uploaded files with names", () => {
    render(<DragDropUpload {...defaultProps} />);

    const dropZone = screen.getByTestId("drop-zone");
    const files = [new File(["content"], "test-image.jpg", { type: "image/jpeg" })];

    fireEvent.drop(dropZone, createDataTransfer(files));

    expect(screen.getByText("test-image.jpg")).toBeInTheDocument();
    expect(screen.getByTestId("remove-file-0")).toBeInTheDocument();
  });

  test("removes files when remove button is clicked", async () => {
    const user = userEvent.setup();
    render(<DragDropUpload {...defaultProps} />);

    const dropZone = screen.getByTestId("drop-zone");
    const files = [
      new File(["content1"], "file1.jpg", { type: "image/jpeg" }),
      new File(["content2"], "file2.jpg", { type: "image/jpeg" }),
    ];

    fireEvent.drop(dropZone, createDataTransfer(files));

    expect(screen.getAllByTestId("file-item")).toHaveLength(2);

    await user.click(screen.getByTestId("remove-file-0"));

    expect(screen.getAllByTestId("file-item")).toHaveLength(1);
    expect(screen.queryByText("file1.jpg")).not.toBeInTheDocument();
    expect(screen.getByText("file2.jpg")).toBeInTheDocument();
  });

  test("prevents dropping too many files", () => {
    render(<DragDropUpload {...defaultProps} maxFiles={2} />);

    const dropZone = screen.getByTestId("drop-zone");
    const files = [
      new File(["content1"], "file1.jpg", { type: "image/jpeg" }),
      new File(["content2"], "file2.jpg", { type: "image/jpeg" }),
      new File(["content3"], "file3.jpg", { type: "image/jpeg" }),
    ];

    fireEvent.drop(dropZone, createDataTransfer(files));

    expect(window.alert).toHaveBeenCalledWith("Maximum 2 files allowed");
    expect(mockOnFileDrop).not.toHaveBeenCalled();
    expect(screen.queryByTestId("file-list")).not.toBeInTheDocument();
  });

  test("considers already uploaded files when checking max limit", () => {
    render(<DragDropUpload {...defaultProps} maxFiles={3} />);

    const dropZone = screen.getByTestId("drop-zone");

    // First drop - 2 files
    const firstBatch = [
      new File(["content1"], "file1.jpg", { type: "image/jpeg" }),
      new File(["content2"], "file2.jpg", { type: "image/jpeg" }),
    ];
    fireEvent.drop(dropZone, createDataTransfer(firstBatch));

    // Second drop - 2 more files (should exceed limit)
    const secondBatch = [
      new File(["content3"], "file3.jpg", { type: "image/jpeg" }),
      new File(["content4"], "file4.jpg", { type: "image/jpeg" }),
    ];
    fireEvent.drop(dropZone, createDataTransfer(secondBatch));

    expect(window.alert).toHaveBeenCalledWith("Maximum 3 files allowed");
    expect(screen.getAllByTestId("file-item")).toHaveLength(2); // Only first batch
  });
});

Testing File Preview Components

Image Preview Component

// ImagePreview.jsx
import React, { useState, useEffect } from "react";

const ImagePreview = ({ file, onRemove, maxWidth = 200 }) => {
  const [preview, setPreview] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    if (!file) return;

    const reader = new FileReader();

    reader.onload = () => {
      setPreview(reader.result);
      setLoading(false);
    };

    reader.onerror = () => {
      setError("Failed to load image preview");
      setLoading(false);
    };

    reader.readAsDataURL(file);

    return () => {
      if (preview) {
        URL.revokeObjectURL(preview);
      }
    };
  }, [file, preview]);

  const formatFileSize = (bytes) => {
    if (bytes === 0) return "0 Bytes";
    const k = 1024;
    const sizes = ["Bytes", "KB", "MB", "GB"];
    const i = Math.floor(Math.log(bytes) / Math.log(k));
    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
  };

  if (loading) {
    return (
      <div className="image-preview loading" data-testid="loading-indicator">
        Loading preview...
      </div>
    );
  }

  if (error) {
    return (
      <div className="image-preview error" data-testid="error-message">
        {error}
      </div>
    );
  }

  return (
    <div className="image-preview" data-testid="image-preview">
      <div className="preview-container">
        <img src={preview} alt={file.name} style={{ maxWidth: `${maxWidth}px` }} data-testid="preview-image" />
        <button
          className="remove-button"
          onClick={onRemove}
          data-testid="remove-button"
          aria-label={`Remove ${file.name}`}
        >
          ×
        </button>
      </div>
      <div className="file-info" data-testid="file-info">
        <p className="file-name" title={file.name}>
          {file.name}
        </p>
        <p className="file-size">{formatFileSize(file.size)}</p>
      </div>
    </div>
  );
};

export default ImagePreview;

Image Preview Testing

// ImagePreview.test.jsx
import React from "react";
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import ImagePreview from "./ImagePreview";

describe("ImagePreview Component", () => {
  const mockOnRemove = jest.fn();
  const mockFile = new File(["test content"], "test-image.jpg", {
    type: "image/jpeg",
  });

  beforeEach(() => {
    mockOnRemove.mockClear();
  });

  test("shows loading state initially", () => {
    render(<ImagePreview file={mockFile} onRemove={mockOnRemove} />);

    expect(screen.getByTestId("loading-indicator")).toHaveTextContent("Loading preview...");
  });

  test("displays image preview after loading", async () => {
    render(<ImagePreview file={mockFile} onRemove={mockOnRemove} />);

    await waitFor(() => {
      expect(screen.getByTestId("image-preview")).toBeInTheDocument();
    });

    const previewImage = screen.getByTestId("preview-image");
    expect(previewImage).toHaveAttribute("src", "data:image/jpeg;base64,mock-base64-content");
    expect(previewImage).toHaveAttribute("alt", "test-image.jpg");
  });

  test("displays file information", async () => {
    const largeFile = new File(["x".repeat(1024 * 1024)], "large-image.png", {
      type: "image/png",
    });

    render(<ImagePreview file={largeFile} onRemove={mockOnRemove} />);

    await waitFor(() => {
      expect(screen.getByTestId("file-info")).toBeInTheDocument();
    });

    expect(screen.getByText("large-image.png")).toBeInTheDocument();
    expect(screen.getByText("1 MB")).toBeInTheDocument();
  });

  test("formats file sizes correctly", async () => {
    const testCases = [
      { size: 0, expected: "0 Bytes" },
      { size: 1024, expected: "1 KB" },
      { size: 1536, expected: "1.5 KB" },
      { size: 1024 * 1024, expected: "1 MB" },
    ];

    for (const testCase of testCases) {
      const file = new File(["x".repeat(testCase.size)], "test.jpg", {
        type: "image/jpeg",
      });
      Object.defineProperty(file, "size", { value: testCase.size });

      const { unmount } = render(<ImagePreview file={file} onRemove={mockOnRemove} />);

      await waitFor(() => {
        expect(screen.getByText(testCase.expected)).toBeInTheDocument();
      });

      unmount();
    }
  });

  test("calls onRemove when remove button is clicked", async () => {
    const user = userEvent.setup();
    render(<ImagePreview file={mockFile} onRemove={mockOnRemove} />);

    await waitFor(() => {
      expect(screen.getByTestId("remove-button")).toBeInTheDocument();
    });

    await user.click(screen.getByTestId("remove-button"));

    expect(mockOnRemove).toHaveBeenCalledTimes(1);
  });

  test("respects maxWidth prop for image sizing", async () => {
    render(<ImagePreview file={mockFile} onRemove={mockOnRemove} maxWidth={150} />);

    await waitFor(() => {
      const previewImage = screen.getByTestId("preview-image");
      expect(previewImage).toHaveStyle("maxWidth: 150px");
    });
  });

  test("handles FileReader errors gracefully", async () => {
    // Mock FileReader to simulate error
    const originalFileReader = global.FileReader;
    global.FileReader = class MockFileReader {
      readAsDataURL() {
        setTimeout(() => {
          this.onerror && this.onerror();
        }, 10);
      }
    };

    render(<ImagePreview file={mockFile} onRemove={mockOnRemove} />);

    await waitFor(() => {
      expect(screen.getByTestId("error-message")).toHaveTextContent("Failed to load image preview");
    });

    // Restore original FileReader
    global.FileReader = originalFileReader;
  });
});

Testing Upload Progress and State Management

Upload with Progress Component

// UploadWithProgress.jsx
import React, { useState } from "react";

const UploadWithProgress = ({ onUpload, endpoint }) => {
  const [files, setFiles] = useState([]);
  const [uploadProgress, setUploadProgress] = useState({});
  const [uploadStatus, setUploadStatus] = useState({});

  const handleFileSelect = (selectedFiles) => {
    const fileArray = Array.from(selectedFiles);
    setFiles((prev) => [...prev, ...fileArray]);

    // Initialize progress and status for new files
    fileArray.forEach((file) => {
      setUploadProgress((prev) => ({ ...prev, [file.name]: 0 }));
      setUploadStatus((prev) => ({ ...prev, [file.name]: "pending" }));
    });
  };

  const uploadFile = async (file) => {
    setUploadStatus((prev) => ({ ...prev, [file.name]: "uploading" }));

    const formData = new FormData();
    formData.append("file", file);

    try {
      const xhr = new XMLHttpRequest();

      return new Promise((resolve, reject) => {
        xhr.upload.addEventListener("progress", (event) => {
          if (event.lengthComputable) {
            const progress = Math.round((event.loaded / event.total) * 100);
            setUploadProgress((prev) => ({ ...prev, [file.name]: progress }));
          }
        });

        xhr.addEventListener("load", () => {
          if (xhr.status === 200) {
            setUploadStatus((prev) => ({ ...prev, [file.name]: "completed" }));
            resolve(JSON.parse(xhr.responseText));
          } else {
            setUploadStatus((prev) => ({ ...prev, [file.name]: "error" }));
            reject(new Error(`Upload failed: ${xhr.statusText}`));
          }
        });

        xhr.addEventListener("error", () => {
          setUploadStatus((prev) => ({ ...prev, [file.name]: "error" }));
          reject(new Error("Upload failed"));
        });

        xhr.open("POST", endpoint);
        xhr.send(formData);
      });
    } catch (error) {
      setUploadStatus((prev) => ({ ...prev, [file.name]: "error" }));
      throw error;
    }
  };

  const uploadAllFiles = async () => {
    const pendingFiles = files.filter((file) => uploadStatus[file.name] === "pending");

    for (const file of pendingFiles) {
      try {
        const result = await uploadFile(file);
        onUpload && onUpload(file, result);
      } catch (error) {
        console.error(`Failed to upload ${file.name}:`, error);
      }
    }
  };

  const removeFile = (fileName) => {
    setFiles((prev) => prev.filter((file) => file.name !== fileName));
    setUploadProgress((prev) => {
      const newProgress = { ...prev };
      delete newProgress[fileName];
      return newProgress;
    });
    setUploadStatus((prev) => {
      const newStatus = { ...prev };
      delete newStatus[fileName];
      return newStatus;
    });
  };

  return (
    <div className="upload-with-progress" data-testid="upload-container">
      <input type="file" multiple onChange={(e) => handleFileSelect(e.target.files)} data-testid="file-input" />

      {files.length > 0 && (
        <div className="file-list" data-testid="file-list">
          {files.map((file) => (
            <div key={file.name} className="file-item" data-testid={`file-${file.name}`}>
              <div className="file-info">
                <span className="file-name">{file.name}</span>
                <span className="file-status" data-testid={`status-${file.name}`}>
                  {uploadStatus[file.name]}
                </span>
              </div>

              {uploadStatus[file.name] === "uploading" && (
                <div className="progress-bar" data-testid={`progress-${file.name}`}>
                  <div className="progress-fill" style={{ width: `${uploadProgress[file.name]}%` }} />
                  <span className="progress-text">{uploadProgress[file.name]}%</span>
                </div>
              )}

              <button
                onClick={() => removeFile(file.name)}
                disabled={uploadStatus[file.name] === "uploading"}
                data-testid={`remove-${file.name}`}
              >
                Remove
              </button>
            </div>
          ))}

          <button
            onClick={uploadAllFiles}
            disabled={files.every((file) => uploadStatus[file.name] !== "pending")}
            data-testid="upload-all-button"
          >
            Upload All Files
          </button>
        </div>
      )}
    </div>
  );
};

export default UploadWithProgress;

Testing Upload Progress

// UploadWithProgress.test.jsx
import React from "react";
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import UploadWithProgress from "./UploadWithProgress";

// Mock XMLHttpRequest
class MockXMLHttpRequest {
  constructor() {
    this.upload = {
      addEventListener: jest.fn(),
    };
    this.addEventListener = jest.fn();
    this.open = jest.fn();
    this.send = jest.fn();
    this.status = 200;
    this.statusText = "OK";
    this.responseText = '{"success": true, "id": "123"}';
  }

  // Method to simulate upload progress
  simulateProgress(percent) {
    const progressHandler = this.upload.addEventListener.mock.calls.find((call) => call[0] === "progress")[1];

    if (progressHandler) {
      progressHandler({
        lengthComputable: true,
        loaded: percent,
        total: 100,
      });
    }
  }

  // Method to simulate upload completion
  simulateSuccess() {
    const loadHandler = this.addEventListener.mock.calls.find((call) => call[0] === "load")[1];

    if (loadHandler) {
      loadHandler();
    }
  }

  // Method to simulate upload error
  simulateError() {
    const errorHandler = this.addEventListener.mock.calls.find((call) => call[0] === "error")[1];

    if (errorHandler) {
      errorHandler();
    }
  }
}

describe("UploadWithProgress Component", () => {
  const mockOnUpload = jest.fn();
  const defaultProps = {
    onUpload: mockOnUpload,
    endpoint: "/api/upload",
  };

  let mockXHR;

  beforeEach(() => {
    mockOnUpload.mockClear();
    mockXHR = new MockXMLHttpRequest();
    global.XMLHttpRequest = jest.fn(() => mockXHR);
  });

  afterEach(() => {
    jest.restoreAllMocks();
  });

  test("renders file input and handles file selection", async () => {
    const user = userEvent.setup();
    render(<UploadWithProgress {...defaultProps} />);

    const fileInput = screen.getByTestId("file-input");
    const files = [
      new File(["content1"], "file1.txt", { type: "text/plain" }),
      new File(["content2"], "file2.txt", { type: "text/plain" }),
    ];

    await user.upload(fileInput, files);

    expect(screen.getByTestId("file-list")).toBeInTheDocument();
    expect(screen.getByTestId("file-file1.txt")).toBeInTheDocument();
    expect(screen.getByTestId("file-file2.txt")).toBeInTheDocument();
  });

  test("initializes files with pending status", async () => {
    const user = userEvent.setup();
    render(<UploadWithProgress {...defaultProps} />);

    const fileInput = screen.getByTestId("file-input");
    const file = new File(["content"], "test.txt", { type: "text/plain" });

    await user.upload(fileInput, file);

    expect(screen.getByTestId("status-test.txt")).toHaveTextContent("pending");
  });

  test("uploads files and shows progress", async () => {
    const user = userEvent.setup();
    render(<UploadWithProgress {...defaultProps} />);

    const fileInput = screen.getByTestId("file-input");
    const file = new File(["content"], "test.txt", { type: "text/plain" });

    await user.upload(fileInput, file);

    const uploadButton = screen.getByTestId("upload-all-button");
    await user.click(uploadButton);

    // Check that upload started
    expect(screen.getByTestId("status-test.txt")).toHaveTextContent("uploading");

    // Simulate progress updates
    mockXHR.simulateProgress(50);
    await waitFor(() => {
      expect(screen.getByTestId("progress-test.txt")).toBeInTheDocument();
      expect(screen.getByText("50%")).toBeInTheDocument();
    });

    mockXHR.simulateProgress(100);
    await waitFor(() => {
      expect(screen.getByText("100%")).toBeInTheDocument();
    });

    // Simulate completion
    mockXHR.simulateSuccess();
    await waitFor(() => {
      expect(screen.getByTestId("status-test.txt")).toHaveTextContent("completed");
    });

    expect(mockOnUpload).toHaveBeenCalledWith(file, { success: true, id: "123" });
  });

  test("handles upload errors", async () => {
    const user = userEvent.setup();
    render(<UploadWithProgress {...defaultProps} />);

    const fileInput = screen.getByTestId("file-input");
    const file = new File(["content"], "test.txt", { type: "text/plain" });

    await user.upload(fileInput, file);

    const uploadButton = screen.getByTestId("upload-all-button");
    await user.click(uploadButton);

    // Simulate error
    mockXHR.simulateError();

    await waitFor(() => {
      expect(screen.getByTestId("status-test.txt")).toHaveTextContent("error");
    });

    expect(mockOnUpload).not.toHaveBeenCalled();
  });

  test("allows removing files", async () => {
    const user = userEvent.setup();
    render(<UploadWithProgress {...defaultProps} />);

    const fileInput = screen.getByTestId("file-input");
    const file = new File(["content"], "test.txt", { type: "text/plain" });

    await user.upload(fileInput, file);

    expect(screen.getByTestId("file-test.txt")).toBeInTheDocument();

    const removeButton = screen.getByTestId("remove-test.txt");
    await user.click(removeButton);

    expect(screen.queryByTestId("file-test.txt")).not.toBeInTheDocument();
  });

  test("disables upload button when no pending files", async () => {
    const user = userEvent.setup();
    render(<UploadWithProgress {...defaultProps} />);

    const fileInput = screen.getByTestId("file-input");
    const file = new File(["content"], "test.txt", { type: "text/plain" });

    await user.upload(fileInput, file);

    const uploadButton = screen.getByTestId("upload-all-button");
    expect(uploadButton).not.toBeDisabled();

    await user.click(uploadButton);
    mockXHR.simulateSuccess();

    await waitFor(() => {
      expect(uploadButton).toBeDisabled();
    });
  });

  test("disables remove button during upload", async () => {
    const user = userEvent.setup();
    render(<UploadWithProgress {...defaultProps} />);

    const fileInput = screen.getByTestId("file-input");
    const file = new File(["content"], "test.txt", { type: "text/plain" });

    await user.upload(fileInput, file);

    const uploadButton = screen.getByTestId("upload-all-button");
    await user.click(uploadButton);

    const removeButton = screen.getByTestId("remove-test.txt");
    expect(removeButton).toBeDisabled();
  });
});

Conclusion

Comprehensive React file upload testing requires attention to multiple aspects:

  1. Component Logic: Test file validation, state management, and user interactions
  2. File API Mocking: Mock File, FileReader, and XMLHttpRequest for reliable tests
  3. Event Simulation: Test drag-and-drop, file selection, and progress tracking
  4. Error Scenarios: Validate error handling and user feedback
  5. Integration: Test component interactions and async operations

These testing patterns ensure robust, user-friendly file upload experiences in React applications while maintaining code quality and reliability.

Next Steps:

  • Add visual regression testing for upload UI states
  • Implement E2E tests for complete upload workflows
  • Test accessibility features and keyboard navigation
  • Add performance testing for large file uploads

Related Articles

Ready to Start Testing?

Put these insights into practice with FileMock. Generate realistic test files and build comprehensive test suites that ensure your applications are production-ready.

Start Testing with FileMock