Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
206 changes: 183 additions & 23 deletions __tests__/components/download-receipt-button.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,17 @@ import { DownloadReceiptButton } from '@/components/streams/download-receipt-but
import type { StreamData } from '@/types/stream'

vi.mock('sonner', () => ({
toast: { success: vi.fn(), error: vi.fn() },
toast: {
success: vi.fn(),
error: vi.fn(),
},
}))

vi.mock('@/lib/receipt-utils', () => ({
buildReceiptData: vi.fn(() => ({ test: 'receipt' })),
generateReceiptCSV: vi.fn(() => 'csv,data\n1,2'),
generateReceiptHTML: vi.fn(() => '<html><body>Receipt</body></html>'),
downloadFile: vi.fn(),
}))

const TOKEN = { address: 'TADDR', symbol: 'USDC', decimals: 7 }
Expand All @@ -28,37 +38,187 @@ function makeStream(): StreamData {
}
}

describe('DownloadReceiptButton openInBrowser', () => {
const blobUrl = 'blob:http://localhost/receipt'

describe('DownloadReceiptButton', () => {
beforeEach(() => {
vi.useFakeTimers()
vi.stubGlobal('URL', {
createObjectURL: vi.fn(() => blobUrl),
revokeObjectURL: vi.fn(),
vi.clearAllMocks()
})

describe('button rendering', () => {
it('renders the button with correct initial state', () => {
render(<DownloadReceiptButton stream={makeStream()} />)

const button = screen.getByRole('button', { name: /download receipt/i })
expect(button).toBeInTheDocument()
expect(button).not.toBeDisabled()
})

it('shows full text on medium screens and above', () => {
render(<DownloadReceiptButton stream={makeStream()} />)
expect(screen.getByText('Download Receipt')).toBeInTheDocument()
})
vi.stubGlobal('open', vi.fn())
})

afterEach(() => {
vi.useRealTimers()
vi.unstubAllGlobals()
describe('CSV download', () => {
it('calls downloadFile with CSV format when Download CSV is clicked', async () => {
const { downloadFile } = await import('@/lib/receipt-utils')

render(<DownloadReceiptButton stream={makeStream()} />)

fireEvent.click(screen.getByRole('button', { name: /download receipt/i }))
fireEvent.click(screen.getByText(/download csv/i))

expect(vi.mocked(downloadFile)).toHaveBeenCalled()
const call = vi.mocked(downloadFile).mock.calls[0]
expect(call[1]).toBe('flowstar-receipt-stream-123.csv')
expect(call[2]).toBe('text/csv')
})

it('downloads CSV when Download CSV is clicked', () => {
render(<DownloadReceiptButton stream={makeStream()} />)

fireEvent.click(screen.getByRole('button', { name: /download receipt/i }))
fireEvent.click(screen.getByText(/download csv/i))

// Successfully clicking the CSV download option completes without error
expect(screen.getByRole('button', { name: /download receipt/i })).toBeInTheDocument()
})
})

it('revokes the receipt blob URL after the new tab has had time to load', () => {
render(<DownloadReceiptButton stream={makeStream()} />)
describe('View & Print HTML', () => {
const blobUrl = 'blob:http://localhost/receipt'

beforeEach(() => {
vi.useFakeTimers()
vi.stubGlobal('URL', {
createObjectURL: vi.fn(() => blobUrl),
revokeObjectURL: vi.fn(),
})
vi.stubGlobal('open', vi.fn())
})

afterEach(() => {
vi.useRealTimers()
vi.unstubAllGlobals()
})

it('creates blob with HTML content when View & Print is clicked', () => {
render(<DownloadReceiptButton stream={makeStream()} />)

fireEvent.click(screen.getByRole('button', { name: /download receipt/i }))
fireEvent.click(screen.getByText(/view & print html/i))

const createObjectURL = vi.mocked((global as any).URL.createObjectURL)
expect(createObjectURL).toHaveBeenCalledTimes(1)
const blobArg = createObjectURL.mock.calls[0][0]
expect(blobArg).toBeInstanceOf(Blob)
expect(blobArg.type).toBe('text/html')
})

it('opens blob URL in new tab with _blank target', () => {
render(<DownloadReceiptButton stream={makeStream()} />)

fireEvent.click(screen.getByRole('button', { name: /download receipt/i }))
fireEvent.click(screen.getByText(/view & print html/i))

const winOpen = vi.mocked((global as any).open)
expect(winOpen).toHaveBeenCalledWith(blobUrl, '_blank')
})

it('revokes the receipt blob URL after timeout', () => {
render(<DownloadReceiptButton stream={makeStream()} />)

fireEvent.click(screen.getByRole('button', { name: /download receipt/i }))
fireEvent.click(screen.getByText(/view & print html/i))
fireEvent.click(screen.getByRole('button', { name: /download receipt/i }))
fireEvent.click(screen.getByText(/view & print html/i))

expect(URL.createObjectURL).toHaveBeenCalledTimes(1)
expect(window.open).toHaveBeenCalledWith(blobUrl, '_blank')
expect(URL.revokeObjectURL).not.toHaveBeenCalled()
const revokeObjectURL = vi.mocked((global as any).URL.revokeObjectURL)
expect(revokeObjectURL).not.toHaveBeenCalled()

vi.advanceTimersByTime(9_999)
expect(URL.revokeObjectURL).not.toHaveBeenCalled()
vi.advanceTimersByTime(9_999)
expect(revokeObjectURL).not.toHaveBeenCalled()

vi.advanceTimersByTime(1)
expect(URL.revokeObjectURL).toHaveBeenCalledWith(blobUrl)
vi.advanceTimersByTime(1)
expect(revokeObjectURL).toHaveBeenCalledWith(blobUrl)
})

it('opens HTML receipt when View & Print is clicked', () => {
render(<DownloadReceiptButton stream={makeStream()} />)

fireEvent.click(screen.getByRole('button', { name: /download receipt/i }))
fireEvent.click(screen.getByText(/view & print html/i))

const winOpen = vi.mocked((global as any).open)
expect(winOpen).toHaveBeenCalledWith(blobUrl, '_blank')
})
})

describe('dropdown menu', () => {
it('shows menu options when button is clicked', () => {
render(<DownloadReceiptButton stream={makeStream()} />)

fireEvent.click(screen.getByRole('button', { name: /download receipt/i }))

expect(screen.getByText(/view & print html/i)).toBeInTheDocument()
expect(screen.getByText(/download csv/i)).toBeInTheDocument()
})

it('both menu items are present and clickable', () => {
render(<DownloadReceiptButton stream={makeStream()} />)

fireEvent.click(screen.getByRole('button', { name: /download receipt/i }))

const viewPrintItem = screen.getByText(/view & print html/i).closest('[role="menuitem"]')
const downloadCsvItem = screen.getByText(/download csv/i).closest('[role="menuitem"]')

expect(viewPrintItem).toBeInTheDocument()
expect(downloadCsvItem).toBeInTheDocument()
})
})

describe('with transaction hashes', () => {
it('renders correctly with creation transaction hash', () => {
render(
<DownloadReceiptButton
stream={makeStream()}
creationTxHash="abc123def456"
/>
)

expect(screen.getByRole('button', { name: /download receipt/i })).toBeInTheDocument()
})

it('renders correctly with withdrawal transaction hashes', () => {
render(
<DownloadReceiptButton
stream={makeStream()}
withdrawalTxHashes={['tx1', 'tx2']}
/>
)

expect(screen.getByRole('button', { name: /download receipt/i })).toBeInTheDocument()
})

it('renders correctly with cancellation transaction hash', () => {
render(
<DownloadReceiptButton
stream={makeStream()}
cancellationTxHash="cancel123"
/>
)

expect(screen.getByRole('button', { name: /download receipt/i })).toBeInTheDocument()
})

it('renders correctly with all transaction hashes', () => {
render(
<DownloadReceiptButton
stream={makeStream()}
creationTxHash="create123"
withdrawalTxHashes={['withdraw1', 'withdraw2']}
cancellationTxHash="cancel123"
/>
)

expect(screen.getByRole('button', { name: /download receipt/i })).toBeInTheDocument()
})
})
})
164 changes: 164 additions & 0 deletions __tests__/components/ui/button.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import { describe, it, expect } from 'vitest'
import { render, screen } from '@testing-library/react'
import { Button, buttonVariants } from '@/components/ui/button'
import React from 'react'

describe('Button', () => {
describe('variants', () => {
it('renders default variant with correct classes', () => {
const { container } = render(<Button>Default</Button>)
const button = container.querySelector('button')
expect(button?.className).toContain('bg-primary')
expect(button?.className).toContain('text-primary-foreground')
})

it('renders outline variant with correct classes', () => {
const { container } = render(<Button variant="outline">Outline</Button>)
const button = container.querySelector('button')
expect(button?.className).toContain('border-border')
expect(button?.className).toContain('bg-background')
})

it('renders secondary variant with correct classes', () => {
const { container } = render(<Button variant="secondary">Secondary</Button>)
const button = container.querySelector('button')
expect(button?.className).toContain('bg-secondary')
expect(button?.className).toContain('text-secondary-foreground')
})

it('renders ghost variant with correct classes', () => {
const { container } = render(<Button variant="ghost">Ghost</Button>)
const button = container.querySelector('button')
expect(button?.className).toContain('hover:bg-muted')
})

it('renders destructive variant with correct classes', () => {
const { container } = render(<Button variant="destructive">Destructive</Button>)
const button = container.querySelector('button')
expect(button?.className).toContain('bg-destructive/10')
expect(button?.className).toContain('text-destructive')
})

it('renders link variant with correct classes', () => {
const { container } = render(<Button variant="link">Link</Button>)
const button = container.querySelector('button')
expect(button?.className).toContain('text-primary')
expect(button?.className).toContain('underline-offset-4')
})
})

describe('sizes', () => {
it('renders default size with correct classes', () => {
const { container } = render(<Button size="default">Default</Button>)
const button = container.querySelector('button')
expect(button?.className).toContain('h-8')
expect(button?.className).toContain('gap-1.5')
expect(button?.className).toContain('px-2.5')
})

it('renders xs size with correct classes', () => {
const { container } = render(<Button size="xs">XS</Button>)
const button = container.querySelector('button')
expect(button?.className).toContain('h-6')
expect(button?.className).toContain('text-xs')
})

it('renders sm size with correct classes', () => {
const { container } = render(<Button size="sm">SM</Button>)
const button = container.querySelector('button')
expect(button?.className).toContain('h-7')
expect(button?.className).toContain('text-[0.8rem]')
})

it('renders lg size with correct classes', () => {
const { container } = render(<Button size="lg">LG</Button>)
const button = container.querySelector('button')
expect(button?.className).toContain('h-9')
})

it('renders icon size with correct classes', () => {
const { container } = render(<Button size="icon">I</Button>)
const button = container.querySelector('button')
expect(button?.className).toContain('size-8')
})

it('renders icon-xs size with correct classes', () => {
const { container } = render(<Button size="icon-xs">I</Button>)
const button = container.querySelector('button')
expect(button?.className).toContain('size-6')
})

it('renders icon-sm size with correct classes', () => {
const { container } = render(<Button size="icon-sm">I</Button>)
const button = container.querySelector('button')
expect(button?.className).toContain('size-7')
})

it('renders icon-lg size with correct classes', () => {
const { container } = render(<Button size="icon-lg">I</Button>)
const button = container.querySelector('button')
expect(button?.className).toContain('size-9')
})
})

describe('asChild prop', () => {
it('renders child element when asChild is true', () => {
const { container } = render(
<Button asChild>
<a href="/test">Link Button</a>
</Button>
)
const link = container.querySelector('a')
expect(link).toBeInTheDocument()
expect(link?.href).toContain('/test')
expect(link?.textContent).toBe('Link Button')
})

it('applies Button styles to child element when asChild is true', () => {
const { container } = render(
<Button variant="secondary" size="sm" asChild>
<a href="/test">Link Button</a>
</Button>
)
const link = container.querySelector('a')
expect(link?.className).toContain('bg-secondary')
expect(link?.className).toContain('text-secondary-foreground')
})

it('renders native button when asChild is false', () => {
const { container } = render(<Button>Native Button</Button>)
const button = container.querySelector('button')
expect(button).toBeInTheDocument()
expect(button?.textContent).toBe('Native Button')
})
})

describe('disabled state', () => {
it('renders disabled button with correct attributes', () => {
const { container } = render(<Button disabled>Disabled</Button>)
const button = container.querySelector('button') as HTMLButtonElement
expect(button.disabled).toBe(true)
expect(button.className).toContain('disabled:opacity-50')
})

it('applies disabled styles', () => {
const { container } = render(<Button disabled>Disabled</Button>)
const button = container.querySelector('button')
expect(button?.className).toContain('disabled:pointer-events-none')
})
})

describe('buttonVariants CVA', () => {
it('generates correct classes for variant and size combination', () => {
const classes = buttonVariants({ variant: 'outline', size: 'lg' })
expect(classes).toContain('border-border')
expect(classes).toContain('h-9')
})

it('uses default variant and size when none specified', () => {
const classes = buttonVariants({})
expect(classes).toContain('bg-primary')
expect(classes).toContain('h-8')
})
})
})
Loading