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()
})
})