import { describe, test, expect, beforeEach, vi } from 'vitest' import { render, screen, fireEvent, waitFor } from '@testing-library/react' import { Download } from './download' import { useWidgetStore } from '../../../widgets/stores' import type { DownloadItem } from './types' describe('Download', () => { const widgetId = 'test-download-widget' beforeEach(() => { // Clear store before each test useWidgetStore.getState().clearWidgets() }) test('disables button when download items array is empty', () => { render() const button = screen.getByRole('button', { name: 'download options' }) expect(button.hasAttribute('disabled')).toBeTruthy() }) test('renders download button when download options exist', () => { const mockDownload: DownloadItem[] = [ { id: 'csv', label: 'CSV', modifier: vi.fn().mockResolvedValue('data:text/csv;base64,'), }, ] useWidgetStore.getState().setWidget(widgetId, { data: [['a', 'b']], }) render() const button = screen.getByRole('button', { name: 'download options' }) expect(button).toBeTruthy() }) test('uses custom aria label when provided', () => { const mockDownload: DownloadItem[] = [ { id: 'csv', label: 'CSV', modifier: vi.fn().mockResolvedValue('data:text/csv;base64,'), }, ] render( , ) const button = screen.getByRole('button', { name: 'custom download' }) expect(button).toBeTruthy() }) test('opens menu when button is clicked', async () => { const mockDownload: DownloadItem[] = [ { id: 'csv', label: 'CSV', modifier: vi.fn().mockResolvedValue('data:text/csv;base64,'), }, { id: 'png', label: 'PNG', modifier: vi.fn().mockResolvedValue('data:image/png;base64,'), }, ] render() const button = screen.getByRole('button', { name: 'download options' }) fireEvent.click(button) await waitFor(() => { expect(screen.getByText('CSV')).toBeTruthy() expect(screen.getByText('PNG')).toBeTruthy() }) }) test('renders menu items with icons when provided', async () => { const mockIcon = Icon const mockDownload: DownloadItem[] = [ { id: 'csv', label: 'CSV', icon: mockIcon, modifier: vi.fn().mockResolvedValue('data:text/csv;base64,'), }, ] render() const button = screen.getByRole('button', { name: 'download options' }) fireEvent.click(button) await waitFor(() => { expect(screen.getByTestId('test-icon')).toBeTruthy() }) }) test('calls modifier and triggers download when menu item is clicked', async () => { const mockModifier = vi.fn().mockResolvedValue('data:text/csv;base64,test') const mockCallback = vi.fn() const mockDownload: DownloadItem[] = [ { id: 'csv', label: 'CSV', filename: 'test.csv', modifier: mockModifier, callback: mockCallback, }, ] useWidgetStore.getState().setWidget(widgetId, { data: [['test', 'data']], }) // Mock document methods const createElementSpy = vi.spyOn(document, 'createElement') const appendChildSpy = vi.spyOn(document.body, 'appendChild') const removeChildSpy = vi.spyOn(document.body, 'removeChild') render() const button = screen.getByRole('button', { name: 'download options' }) fireEvent.click(button) const csvMenuItem = await screen.findByText('CSV') fireEvent.click(csvMenuItem) await waitFor(() => { expect(mockModifier).toHaveBeenCalledWith([['test', 'data']]) expect(createElementSpy).toHaveBeenCalledWith('a') expect(mockCallback).toHaveBeenCalled() }) createElementSpy.mockRestore() appendChildSpy.mockRestore() removeChildSpy.mockRestore() }) test('shows loading spinner during download', async () => { const mockModifier = vi.fn().mockImplementation( () => new Promise((resolve) => { setTimeout(() => resolve('data:text/csv;base64,test'), 100) }), ) const mockDownload: DownloadItem[] = [ { id: 'csv', label: 'CSV', modifier: mockModifier, }, ] render() const button = screen.getByRole('button', { name: 'download options' }) fireEvent.click(button) const csvMenuItem = await screen.findByText('CSV') fireEvent.click(csvMenuItem) // Check that loading spinner is shown during download await waitFor(() => { expect(screen.getByRole('progressbar')).toBeTruthy() }) // Wait for download to complete and spinner to disappear await waitFor( () => { expect(screen.queryByRole('progressbar')).toBeNull() }, { timeout: 200 }, ) }) test('handles disabled menu items', async () => { const mockDownload: DownloadItem[] = [ { id: 'csv', label: 'CSV', disabled: true, modifier: vi.fn().mockResolvedValue('data:text/csv;base64,'), }, ] render() const button = screen.getByRole('button', { name: 'download options' }) fireEvent.click(button) const csvMenuItem = await screen.findByText('CSV') expect(csvMenuItem.closest('li')?.getAttribute('aria-disabled')).toBe( 'true', ) }) test('closes menu after clicking a menu item', async () => { const mockDownload: DownloadItem[] = [ { id: 'csv', label: 'CSV', modifier: vi.fn().mockResolvedValue('data:text/csv;base64,'), }, ] render() const button = screen.getByRole('button', { name: 'download options' }) fireEvent.click(button) const csvMenuItem = await screen.findByText('CSV') fireEvent.click(csvMenuItem) await waitFor(() => { expect(screen.queryByRole('menu')).toBeNull() }) }) test('does not download if modifier returns undefined', async () => { const mockModifier = vi.fn().mockResolvedValue(undefined) const mockCallback = vi.fn() const mockDownload: DownloadItem[] = [ { id: 'csv', label: 'CSV', modifier: mockModifier, callback: mockCallback, }, ] const createElementSpy = vi.spyOn(document, 'createElement') render() const button = screen.getByRole('button', { name: 'download options' }) fireEvent.click(button) const csvMenuItem = await screen.findByText('CSV') fireEvent.click(csvMenuItem) await waitFor(() => { expect(mockModifier).toHaveBeenCalled() }) // Should not create download link (check for 'a' element specifically) expect(createElementSpy).not.toHaveBeenCalledWith('a') expect(mockCallback).not.toHaveBeenCalled() createElementSpy.mockRestore() }) test('stops event propagation on button click', () => { const mockDownload: DownloadItem[] = [ { id: 'csv', label: 'CSV', modifier: vi.fn().mockResolvedValue('data:text/csv;base64,'), }, ] const parentClickHandler = vi.fn() render(
, ) const button = screen.getByRole('button', { name: 'download options' }) fireEvent.click(button) expect(parentClickHandler).not.toHaveBeenCalled() }) test('uses widget id as default filename when filename is not provided', async () => { const mockModifier = vi.fn().mockResolvedValue('data:text/csv;base64,test') const mockDownload: DownloadItem[] = [ { id: 'csv', label: 'CSV', modifier: mockModifier, }, ] let capturedLink: HTMLAnchorElement | null = null const originalCreateElement = document.createElement.bind(document) const createElementSpy = vi .spyOn(document, 'createElement') .mockImplementation((tagName: string) => { const element = originalCreateElement(tagName) if (tagName === 'a') { capturedLink = element as HTMLAnchorElement } return element }) render() const button = screen.getByRole('button', { name: 'download options' }) fireEvent.click(button) const csvMenuItem = await screen.findByText('CSV') fireEvent.click(csvMenuItem) await waitFor(() => { expect(capturedLink?.download).toBe(widgetId) }) createElementSpy.mockRestore() }) })