fd2c50c1aa
Fixes two issues reported in #165: - Export always produced a black background, making prints with a white page wasteful/unreadable. Adds a Dark/White background choice in the export modal, threaded through both PNG and SVG export. - Firefox refused the download because the programmatic anchor was not attached to the document. The anchor is now appended before click and removed after, which Firefox requires. Closes #165 ha-relevant: yes
126 lines
5.8 KiB
TypeScript
126 lines
5.8 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
|
||
import { ExportModal } from '../ExportModal'
|
||
|
||
const mockExportToPng = vi.fn()
|
||
const mockExportToSvg = vi.fn()
|
||
vi.mock('@/utils/export', () => ({
|
||
exportToPng: (...args: unknown[]) => mockExportToPng(...args),
|
||
exportToSvg: (...args: unknown[]) => mockExportToSvg(...args),
|
||
EXPORT_QUALITY_OPTIONS: [
|
||
{ value: 'standard', label: 'Standard', pixelRatio: 1, hint: '1× — small file' },
|
||
{ value: 'high', label: 'High', pixelRatio: 2, hint: '2× — recommended' },
|
||
{ value: 'ultra', label: 'Ultra', pixelRatio: 4, hint: '4× — print quality, large file' },
|
||
],
|
||
EXPORT_BACKGROUND_OPTIONS: [
|
||
{ value: 'dark', label: 'Dark', color: '#0d1117', hint: 'screen / docs' },
|
||
{ value: 'white', label: 'White', color: '#ffffff', hint: 'printing' },
|
||
],
|
||
}))
|
||
|
||
const el = document.createElement('div')
|
||
const getElement = () => el
|
||
const onClose = vi.fn()
|
||
|
||
describe('ExportModal', () => {
|
||
beforeEach(() => {
|
||
vi.clearAllMocks()
|
||
mockExportToPng.mockResolvedValue(undefined)
|
||
mockExportToSvg.mockResolvedValue(undefined)
|
||
})
|
||
|
||
it('renders all three quality options', () => {
|
||
render(<ExportModal open onClose={onClose} getElement={getElement} />)
|
||
expect(screen.getByText('Standard')).toBeInTheDocument()
|
||
expect(screen.getByText('High')).toBeInTheDocument()
|
||
expect(screen.getByText('Ultra')).toBeInTheDocument()
|
||
})
|
||
|
||
it('selects High by default', () => {
|
||
render(<ExportModal open onClose={onClose} getElement={getElement} />)
|
||
const highBtn = screen.getByText('High').closest('button')!
|
||
expect(highBtn.className).toContain('border-[#00d4ff]')
|
||
})
|
||
|
||
it('changes selection when another option is clicked', () => {
|
||
render(<ExportModal open onClose={onClose} getElement={getElement} />)
|
||
fireEvent.click(screen.getByText('Ultra').closest('button')!)
|
||
expect(screen.getByText('Ultra').closest('button')!.className).toContain('border-[#00d4ff]')
|
||
expect(screen.getByText('High').closest('button')!.className).not.toContain('border-[#00d4ff]')
|
||
})
|
||
|
||
it('calls exportToPng with selected quality on Download click', async () => {
|
||
render(<ExportModal open onClose={onClose} getElement={getElement} />)
|
||
fireEvent.click(screen.getByText('Standard').closest('button')!)
|
||
fireEvent.click(screen.getByRole('button', { name: /download/i }))
|
||
await waitFor(() => expect(mockExportToPng).toHaveBeenCalledWith(el, 'standard', 'dark'))
|
||
})
|
||
|
||
it('renders an SVG option under the quality options', () => {
|
||
render(<ExportModal open onClose={onClose} getElement={getElement} />)
|
||
expect(screen.getByText('SVG')).toBeInTheDocument()
|
||
})
|
||
|
||
it('selects SVG and calls exportToSvg on Download click', async () => {
|
||
render(<ExportModal open onClose={onClose} getElement={getElement} />)
|
||
fireEvent.click(screen.getByText('SVG').closest('button')!)
|
||
expect(screen.getByText('SVG').closest('button')!.className).toContain('border-[#00d4ff]')
|
||
fireEvent.click(screen.getByRole('button', { name: /download/i }))
|
||
await waitFor(() => expect(mockExportToSvg).toHaveBeenCalledWith(el, 'dark'))
|
||
expect(mockExportToPng).not.toHaveBeenCalled()
|
||
})
|
||
|
||
it('switches back to PNG when a quality option is clicked after SVG', () => {
|
||
render(<ExportModal open onClose={onClose} getElement={getElement} />)
|
||
fireEvent.click(screen.getByText('SVG').closest('button')!)
|
||
fireEvent.click(screen.getByText('Ultra').closest('button')!)
|
||
expect(screen.getByText('Ultra').closest('button')!.className).toContain('border-[#00d4ff]')
|
||
expect(screen.getByText('SVG').closest('button')!.className).not.toContain('border-[#00d4ff]')
|
||
})
|
||
|
||
it('renders dark and white background options, dark selected by default', () => {
|
||
render(<ExportModal open onClose={onClose} getElement={getElement} />)
|
||
expect(screen.getByText('Dark')).toBeInTheDocument()
|
||
expect(screen.getByText('White')).toBeInTheDocument()
|
||
expect(screen.getByText('Dark').closest('button')!.className).toContain('border-[#00d4ff]')
|
||
})
|
||
|
||
it('exports with white background when White is selected (printing)', async () => {
|
||
render(<ExportModal open onClose={onClose} getElement={getElement} />)
|
||
fireEvent.click(screen.getByText('White').closest('button')!)
|
||
fireEvent.click(screen.getByRole('button', { name: /download/i }))
|
||
await waitFor(() => expect(mockExportToPng).toHaveBeenCalledWith(el, 'high', 'white'))
|
||
})
|
||
|
||
it('applies the background choice to SVG export too', async () => {
|
||
render(<ExportModal open onClose={onClose} getElement={getElement} />)
|
||
fireEvent.click(screen.getByText('SVG').closest('button')!)
|
||
fireEvent.click(screen.getByText('White').closest('button')!)
|
||
fireEvent.click(screen.getByRole('button', { name: /download/i }))
|
||
await waitFor(() => expect(mockExportToSvg).toHaveBeenCalledWith(el, 'white'))
|
||
})
|
||
|
||
it('closes after successful export', async () => {
|
||
render(<ExportModal open onClose={onClose} getElement={getElement} />)
|
||
fireEvent.click(screen.getByRole('button', { name: /download/i }))
|
||
await waitFor(() => expect(onClose).toHaveBeenCalled())
|
||
})
|
||
|
||
it('calls onClose when Cancel is clicked', () => {
|
||
render(<ExportModal open onClose={onClose} getElement={getElement} />)
|
||
fireEvent.click(screen.getByRole('button', { name: /cancel/i }))
|
||
expect(onClose).toHaveBeenCalled()
|
||
})
|
||
|
||
it('does not call exportToPng when getElement returns null', async () => {
|
||
render(<ExportModal open onClose={onClose} getElement={() => null} />)
|
||
fireEvent.click(screen.getByRole('button', { name: /download/i }))
|
||
await waitFor(() => expect(mockExportToPng).not.toHaveBeenCalled())
|
||
})
|
||
|
||
it('does not render when closed', () => {
|
||
render(<ExportModal open={false} onClose={onClose} getElement={getElement} />)
|
||
expect(screen.queryByText('Export Canvas')).not.toBeInTheDocument()
|
||
})
|
||
})
|