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
43 changes: 43 additions & 0 deletions __tests__/components/mock-mode-banner.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { describe, it, expect, vi, afterEach } from 'vitest'
import { render, screen } from '@testing-library/react'
import { MockModeBanner } from '@/components/layout/mock-mode-banner'

const mockUseNetwork = vi.fn()

vi.mock('@/components/providers/network-provider', () => ({
useNetwork: () => mockUseNetwork(),
}))

describe('MockModeBanner', () => {
afterEach(() => {
vi.unstubAllEnvs()
vi.clearAllMocks()
})

it('renders the banner when in mock mode outside of production', () => {
vi.stubEnv('NODE_ENV', 'test')
mockUseNetwork.mockReturnValue({ isMockMode: true })

render(<MockModeBanner />)

expect(screen.getByRole('status')).toHaveTextContent('Mock mode — no contract connected')
})

it('renders nothing when a real contract is configured', () => {
vi.stubEnv('NODE_ENV', 'test')
mockUseNetwork.mockReturnValue({ isMockMode: false })

const { container } = render(<MockModeBanner />)

expect(container).toBeEmptyDOMElement()
})

it('renders nothing in production even when in mock mode', () => {
vi.stubEnv('NODE_ENV', 'production')
mockUseNetwork.mockReturnValue({ isMockMode: true })

const { container } = render(<MockModeBanner />)

expect(container).toBeEmptyDOMElement()
})
})
157 changes: 157 additions & 0 deletions __tests__/components/streams-bulk-actions.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import type { StreamData } from '@/types/stream'

// ── Hoisted fixtures / mocks ────────────────────────────────────────────────

const NOW = 1_000_000

const STREAMS = vi.hoisted((): StreamData[] => [
{
id: 'withdrawable-1',
sender: 'GOTHER',
recipient: 'GSENDER',
token: { address: 'CUSDC', symbol: 'USDC', decimals: 7 },
depositedAmount: 1_100n,
withdrawnAmount: 0n,
startTime: BigInt(1_000_000 - 100),
endTime: BigInt(1_000_000 + 1_000),
cliffTime: BigInt(1_000_000 - 100),
cliffAmount: 0n,
amountPerSecond: 1n,
linearAmount: 1_100n,
duration: 1_100n,
cancelled: false,
},
{
id: 'cancellable-1',
sender: 'GSENDER',
recipient: 'GOTHER',
token: { address: 'CUSDC', symbol: 'USDC', decimals: 7 },
depositedAmount: 1_100n,
withdrawnAmount: 0n,
startTime: BigInt(1_000_000 - 100),
endTime: BigInt(1_000_000 + 1_000),
cliffTime: BigInt(1_000_000 - 100),
cliffAmount: 0n,
amountPerSecond: 1n,
linearAmount: 1_100n,
duration: 1_100n,
cancelled: false,
},
])

const mockWithdraw = vi.hoisted(() => vi.fn(() => Promise.resolve('tx-hash')))
const mockCancel = vi.hoisted(() => vi.fn(() => Promise.resolve('tx-hash')))

vi.mock('next/navigation', () => ({
useRouter: () => ({ replace: vi.fn() }),
useSearchParams: () => new URLSearchParams(),
}))

vi.mock('@/hooks/use-wallet', () => ({
useWallet: () => ({ address: 'GSENDER', isConnected: true, reconnecting: false }),
}))

vi.mock('@/hooks/use-streams', () => ({
useStreams: () => ({ all: STREAMS }),
}))

vi.mock('@/hooks/use-now', () => ({
useNow: () => 1_000_000,
}))

vi.mock('@/hooks/use-contract', () => ({
useContract: () => ({ withdraw: mockWithdraw, cancel: mockCancel }),
}))

vi.mock('@/components/streams/stream-card', () => ({
StreamCard: ({
stream,
selectable,
selected,
onToggleSelect,
}: {
stream: StreamData
selectable?: boolean
selected?: boolean
onToggleSelect?: (id: string) => void
}) => (
<div data-testid={`stream-card-${stream.id}`}>
{selectable && (
<input
type="checkbox"
aria-label={`Select stream ${stream.id}`}
data-testid={`stream-card-select-${stream.id}`}
checked={!!selected}
onChange={() => onToggleSelect?.(stream.id)}
/>
)}
{stream.id}
</div>
),
}))

import StreamsRoute from '@/app/app/streams/page'

describe('Streams page bulk select + bulk actions', () => {
beforeEach(() => {
mockWithdraw.mockClear()
mockCancel.mockClear()
})

it('withdraws only the selected streams the wallet can withdraw from', async () => {
render(<StreamsRoute />)

fireEvent.click(screen.getByTestId('bulk-select-toggle'))
fireEvent.click(screen.getByTestId('stream-card-select-withdrawable-1'))
fireEvent.click(screen.getByTestId('stream-card-select-cancellable-1'))

expect(screen.getByTestId('bulk-selected-count')).toHaveTextContent('2 selected')
// Only one of the two selected streams is withdrawable by this wallet.
expect(screen.getByTestId('bulk-withdraw-button')).toHaveTextContent('Withdraw (1)')

fireEvent.click(screen.getByTestId('bulk-withdraw-button'))

await waitFor(() => {
expect(screen.getByTestId('bulk-results')).toHaveTextContent('1 succeeded, 0 failed')
})

expect(mockWithdraw).toHaveBeenCalledTimes(1)
expect(mockWithdraw).toHaveBeenCalledWith('withdrawable-1', 100n)
expect(mockCancel).not.toHaveBeenCalled()
})

it('cancels only the selected streams the wallet can cancel', async () => {
render(<StreamsRoute />)

fireEvent.click(screen.getByTestId('bulk-select-toggle'))
fireEvent.click(screen.getByTestId('stream-card-select-withdrawable-1'))
fireEvent.click(screen.getByTestId('stream-card-select-cancellable-1'))

expect(screen.getByTestId('bulk-cancel-button')).toHaveTextContent('Cancel (1)')

fireEvent.click(screen.getByTestId('bulk-cancel-button'))

await waitFor(() => {
expect(screen.getByTestId('bulk-results')).toHaveTextContent('1 succeeded, 0 failed')
})

expect(mockCancel).toHaveBeenCalledTimes(1)
expect(mockCancel).toHaveBeenCalledWith('cancellable-1')
expect(mockWithdraw).not.toHaveBeenCalled()
})

it('exiting select mode clears the selection and results', () => {
render(<StreamsRoute />)

fireEvent.click(screen.getByTestId('bulk-select-toggle'))
fireEvent.click(screen.getByTestId('stream-card-select-withdrawable-1'))
expect(screen.getByTestId('bulk-selected-count')).toHaveTextContent('1 selected')

fireEvent.click(screen.getByTestId('bulk-select-toggle'))

expect(screen.queryByTestId('bulk-selected-count')).not.toBeInTheDocument()
expect(screen.getByTestId('bulk-select-toggle')).toHaveTextContent('Select')
})
})
35 changes: 35 additions & 0 deletions __tests__/components/usd-toggle.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { describe, it, expect, vi, afterEach } from 'vitest'
import { render, screen, fireEvent } from '@testing-library/react'
import { UsdToggle } from '@/app/app/settings/usd-toggle'

const mockSetShowUsd = vi.fn()
let currentShowUsd = true

vi.mock('@/hooks/use-show-usd', () => ({
useShowUsd: () => [currentShowUsd, mockSetShowUsd],
}))

describe('UsdToggle', () => {
afterEach(() => {
vi.clearAllMocks()
})

it('reflects the current value from useShowUsd as checked', () => {
currentShowUsd = true
render(<UsdToggle />)
expect(screen.getByRole('checkbox')).toBeChecked()
})

it('reflects a false value from useShowUsd as unchecked', () => {
currentShowUsd = false
render(<UsdToggle />)
expect(screen.getByRole('checkbox')).not.toBeChecked()
})

it('calls setShowUsd with the new value when toggled', () => {
currentShowUsd = false
render(<UsdToggle />)
fireEvent.click(screen.getByRole('checkbox'))
expect(mockSetShowUsd).toHaveBeenCalledWith(true)
})
})
44 changes: 44 additions & 0 deletions __tests__/hooks/use-bulk-actions.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { describe, it, expect, vi } from 'vitest'
import { renderHook, act, waitFor } from '@testing-library/react'
import { useBulkActions } from '@/hooks/use-bulk-actions'

describe('useBulkActions', () => {
it('tracks per-item success and failure and reports final status', async () => {
const { result } = renderHook(() => useBulkActions())

const action = vi.fn(async (id: string) => {
if (id === 'bad') throw new Error('boom')
})

act(() => {
result.current.runBulk(['good-1', 'bad', 'good-2'], action)
})

await waitFor(() => expect(result.current.status).toBe('done'))

expect(action).toHaveBeenCalledTimes(3)
expect(result.current.succeeded).toBe(2)
expect(result.current.failed).toBe(1)
expect(result.current.progress).toEqual({ done: 3, total: 3 })
expect(result.current.results).toEqual([
{ id: 'good-1', success: true },
{ id: 'bad', success: false, error: 'boom' },
{ id: 'good-2', success: true },
])
})

it('resets to idle with empty results', async () => {
const { result } = renderHook(() => useBulkActions())

act(() => {
result.current.runBulk(['a'], async () => {})
})
await waitFor(() => expect(result.current.status).toBe('done'))

act(() => result.current.reset())

expect(result.current.status).toBe('idle')
expect(result.current.results).toEqual([])
expect(result.current.progress).toEqual({ done: 0, total: 0 })
})
})
52 changes: 52 additions & 0 deletions __tests__/hooks/use-bulk-select.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { describe, it, expect } from 'vitest'
import { renderHook, act } from '@testing-library/react'
import { useBulkSelect } from '@/hooks/use-bulk-select'

const ITEMS = [{ id: 'a' }, { id: 'b' }, { id: 'c' }]

describe('useBulkSelect', () => {
it('starts with nothing selected', () => {
const { result } = renderHook(() => useBulkSelect(ITEMS))
expect(result.current.someSelected).toBe(false)
expect(result.current.allSelected).toBe(false)
expect(result.current.selectedItems).toEqual([])
})

it('toggle adds and removes an id', () => {
const { result } = renderHook(() => useBulkSelect(ITEMS))

act(() => result.current.toggle('a'))
expect(result.current.selectedItems).toEqual([{ id: 'a' }])
expect(result.current.someSelected).toBe(true)
expect(result.current.allSelected).toBe(false)

act(() => result.current.toggle('a'))
expect(result.current.selectedItems).toEqual([])
expect(result.current.someSelected).toBe(false)
})

it('toggleAll selects everything, then clears when already fully selected', () => {
const { result } = renderHook(() => useBulkSelect(ITEMS))

act(() => result.current.toggleAll())
expect(result.current.allSelected).toBe(true)
expect(result.current.selectedItems).toHaveLength(3)

act(() => result.current.toggleAll())
expect(result.current.allSelected).toBe(false)
expect(result.current.selectedItems).toEqual([])
})

it('clear empties the selection', () => {
const { result } = renderHook(() => useBulkSelect(ITEMS))

act(() => {
result.current.toggle('a')
result.current.toggle('b')
})
expect(result.current.someSelected).toBe(true)

act(() => result.current.clear())
expect(result.current.someSelected).toBe(false)
})
})
Loading
Loading