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
171 changes: 171 additions & 0 deletions components/layout/notification-bell.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import { render, screen, fireEvent } from '@testing-library/react'
import { NotificationBell } from './notification-bell'

jest.mock('@/hooks/use-notifications', () => ({
useNotifications: jest.fn(),
}))

const mockUseNotifications = require('@/hooks/use-notifications').useNotifications as jest.MockedFunction<any>

describe('NotificationBell', () => {
const mockNotifications = [
{ id: '1', message: 'Stream created', type: 'success', read: false },
{ id: '2', message: 'Payment received', type: 'info', read: false },
]

beforeEach(() => {
jest.clearAllMocks()
mockUseNotifications.mockReturnValue({
notifications: mockNotifications,
unreadCount: 2,
markAllRead: jest.fn(),
markAsRead: jest.fn(),
clearNotifications: jest.fn(),
})
})

describe('unread count badge', () => {
it('should display unread count badge', () => {
render(<NotificationBell />)
expect(screen.getByText('2')).toBeInTheDocument()
})

it('should not display badge when unread count is 0', () => {
mockUseNotifications.mockReturnValue({
notifications: [],
unreadCount: 0,
markAllRead: jest.fn(),
markAsRead: jest.fn(),
clearNotifications: jest.fn(),
})

const { container } = render(<NotificationBell />)
expect(screen.queryByText('0')).not.toBeInTheDocument()
})

it('should display 99+ when unread count exceeds 99', () => {
mockUseNotifications.mockReturnValue({
notifications: Array(105).fill({ id: '1', message: 'test', type: 'info', read: false }),
unreadCount: 105,
markAllRead: jest.fn(),
markAsRead: jest.fn(),
clearNotifications: jest.fn(),
})

render(<NotificationBell />)
expect(screen.getByText('99+')).toBeInTheDocument()
})
})

describe('markAllRead on panel open', () => {
it('should call markAllRead when notification panel opens', () => {
const markAllReadMock = jest.fn()
mockUseNotifications.mockReturnValue({
notifications: mockNotifications,
unreadCount: 2,
markAllRead: markAllReadMock,
markAsRead: jest.fn(),
clearNotifications: jest.fn(),
})

render(<NotificationBell />)
const bellButton = screen.getByRole('button')
fireEvent.click(bellButton)

expect(markAllReadMock).toHaveBeenCalled()
})
})

describe('Escape-to-close behavior', () => {
it('should close notification panel when Escape is pressed', () => {
render(<NotificationBell />)
const bellButton = screen.getByRole('button')

// Open the panel
fireEvent.click(bellButton)
expect(screen.getByRole('dialog') || screen.getByText(/notification/i)).toBeInTheDocument()

// Press Escape
fireEvent.keyDown(document, { key: 'Escape', code: 'Escape' })

// Panel should be closed (depending on implementation)
})

it('should not close when other keys are pressed', () => {
render(<NotificationBell />)
const bellButton = screen.getByRole('button')
fireEvent.click(bellButton)

fireEvent.keyDown(document, { key: 'Enter', code: 'Enter' })

// Should still be open or handle gracefully
expect(bellButton).toBeInTheDocument()
})
})

describe('empty state', () => {
it('should render "No notifications yet" when no notifications exist', () => {
mockUseNotifications.mockReturnValue({
notifications: [],
unreadCount: 0,
markAllRead: jest.fn(),
markAsRead: jest.fn(),
clearNotifications: jest.fn(),
})

render(<NotificationBell />)
const bellButton = screen.getByRole('button')
fireEvent.click(bellButton)

expect(screen.getByText(/no notifications yet/i)).toBeInTheDocument()
})

it('should show empty state message when all notifications are cleared', () => {
const clearMock = jest.fn()
mockUseNotifications.mockReturnValue({
notifications: [],
unreadCount: 0,
markAllRead: jest.fn(),
markAsRead: jest.fn(),
clearNotifications: clearMock,
})

render(<NotificationBell />)
const bellButton = screen.getByRole('button')
fireEvent.click(bellButton)

expect(screen.getByText(/no notifications yet/i)).toBeInTheDocument()
})
})

describe('notification interactions', () => {
it('should call markAsRead when a notification is clicked', () => {
const markAsReadMock = jest.fn()
mockUseNotifications.mockReturnValue({
notifications: mockNotifications,
unreadCount: 2,
markAllRead: jest.fn(),
markAsRead: markAsReadMock,
clearNotifications: jest.fn(),
})

render(<NotificationBell />)
const bellButton = screen.getByRole('button')
fireEvent.click(bellButton)

const notification = screen.getByText('Stream created')
fireEvent.click(notification)

expect(markAsReadMock).toHaveBeenCalledWith('1')
})

it('should display multiple notifications in the panel', () => {
render(<NotificationBell />)
const bellButton = screen.getByRole('button')
fireEvent.click(bellButton)

expect(screen.getByText('Stream created')).toBeInTheDocument()
expect(screen.getByText('Payment received')).toBeInTheDocument()
})
})
})
216 changes: 216 additions & 0 deletions components/streams/activity-feed.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
import { render, screen, fireEvent } from '@testing-library/react'
import { ActivityFeed } from './activity-feed'

jest.mock('@/hooks/use-stream-activity', () => ({
useStreamActivity: jest.fn(),
}))

const mockUseStreamActivity = require('@/hooks/use-stream-activity').useStreamActivity as jest.MockedFunction<any>

describe('ActivityFeed', () => {
const mockEvents = [
{ id: '1', type: 'created', timestamp: Date.now(), description: 'Stream created' },
{ id: '2', type: 'deposit', timestamp: Date.now(), description: 'Deposit received' },
{ id: '3', type: 'withdrawal', timestamp: Date.now(), description: 'Funds withdrawn' },
]

beforeEach(() => {
jest.clearAllMocks()
mockUseStreamActivity.mockReturnValue({
events: mockEvents,
hasMore: false,
loading: false,
loadMore: jest.fn(),
filterByType: jest.fn(),
selectedFilter: 'all',
})
})

describe('empty state', () => {
it('should render "No activity yet" when no events exist', () => {
mockUseStreamActivity.mockReturnValue({
events: [],
hasMore: false,
loading: false,
loadMore: jest.fn(),
filterByType: jest.fn(),
selectedFilter: 'all',
})

render(<ActivityFeed streamId="stream-123" />)
expect(screen.getByText(/no activity yet/i)).toBeInTheDocument()
})

it('should render only empty state message and not load-more button', () => {
mockUseStreamActivity.mockReturnValue({
events: [],
hasMore: true,
loading: false,
loadMore: jest.fn(),
filterByType: jest.fn(),
selectedFilter: 'all',
})

render(<ActivityFeed streamId="stream-123" />)
expect(screen.getByText(/no activity yet/i)).toBeInTheDocument()
expect(screen.queryByText(/load more/i)).not.toBeInTheDocument()
})
})

describe('pagination with hasMore', () => {
it('should not display load-more button when hasMore is false', () => {
mockUseStreamActivity.mockReturnValue({
events: mockEvents,
hasMore: false,
loading: false,
loadMore: jest.fn(),
filterByType: jest.fn(),
selectedFilter: 'all',
})

render(<ActivityFeed streamId="stream-123" />)
expect(screen.queryByText(/load more/i)).not.toBeInTheDocument()
})

it('should display load-more button when hasMore is true', () => {
mockUseStreamActivity.mockReturnValue({
events: mockEvents,
hasMore: true,
loading: false,
loadMore: jest.fn(),
filterByType: jest.fn(),
selectedFilter: 'all',
})

render(<ActivityFeed streamId="stream-123" />)
expect(screen.getByText(/load more/i)).toBeInTheDocument()
})

it('should call loadMore when load-more button is clicked', () => {
const loadMoreMock = jest.fn()
mockUseStreamActivity.mockReturnValue({
events: mockEvents,
hasMore: true,
loading: false,
loadMore: loadMoreMock,
filterByType: jest.fn(),
selectedFilter: 'all',
})

render(<ActivityFeed streamId="stream-123" />)
const loadMoreButton = screen.getByText(/load more/i)
fireEvent.click(loadMoreButton)

expect(loadMoreMock).toHaveBeenCalled()
})

it('should disable load-more button while loading', () => {
mockUseStreamActivity.mockReturnValue({
events: mockEvents,
hasMore: true,
loading: true,
loadMore: jest.fn(),
filterByType: jest.fn(),
selectedFilter: 'all',
})

render(<ActivityFeed streamId="stream-123" />)
const loadMoreButton = screen.getByText(/load more/i) as HTMLButtonElement
expect(loadMoreButton.disabled).toBe(true)
})
})

describe('event type filtering', () => {
it('should render filter buttons for event types', () => {
render(<ActivityFeed streamId="stream-123" />)
expect(screen.getByText(/all/i)).toBeInTheDocument()
expect(screen.getByText(/created/i) || screen.getByText(/deposit/i)).toBeInTheDocument()
})

it('should show only events matching selected filter', () => {
const filterByTypeMock = jest.fn()
mockUseStreamActivity.mockReturnValue({
events: [mockEvents[1], mockEvents[2]], // Only deposit and withdrawal
hasMore: false,
loading: false,
loadMore: jest.fn(),
filterByType: filterByTypeMock,
selectedFilter: 'deposit',
})

render(<ActivityFeed streamId="stream-123" />)

// Events should be filtered
expect(screen.getByText('Deposit received')).toBeInTheDocument()
expect(screen.getByText('Funds withdrawn')).toBeInTheDocument()
expect(screen.queryByText('Stream created')).not.toBeInTheDocument()
})

it('should call filterByType when filter button is clicked', () => {
const filterByTypeMock = jest.fn()
mockUseStreamActivity.mockReturnValue({
events: mockEvents,
hasMore: false,
loading: false,
loadMore: jest.fn(),
filterByType: filterByTypeMock,
selectedFilter: 'all',
})

render(<ActivityFeed streamId="stream-123" />)
const depositFilter = screen.getByText(/deposit/i)
fireEvent.click(depositFilter)

expect(filterByTypeMock).toHaveBeenCalledWith('deposit')
})

it('should highlight the selected filter button', () => {
mockUseStreamActivity.mockReturnValue({
events: mockEvents,
hasMore: false,
loading: false,
loadMore: jest.fn(),
filterByType: jest.fn(),
selectedFilter: 'deposit',
})

render(<ActivityFeed streamId="stream-123" />)
const depositFilter = screen.getByText(/deposit/i).closest('button')

expect(depositFilter).toHaveClass('active') || expect(depositFilter).toHaveClass('bg-secondary')
})
})

describe('event display', () => {
it('should display all events in feed', () => {
render(<ActivityFeed streamId="stream-123" />)

mockEvents.forEach(event => {
expect(screen.getByText(event.description)).toBeInTheDocument()
})
})

it('should display events in reverse chronological order', () => {
const orderedEvents = [
{ id: '1', type: 'created', timestamp: 1000, description: 'Event 1' },
{ id: '2', type: 'deposit', timestamp: 2000, description: 'Event 2' },
{ id: '3', type: 'withdrawal', timestamp: 3000, description: 'Event 3' },
]

mockUseStreamActivity.mockReturnValue({
events: orderedEvents,
hasMore: false,
loading: false,
loadMore: jest.fn(),
filterByType: jest.fn(),
selectedFilter: 'all',
})

render(<ActivityFeed streamId="stream-123" />)

const eventElements = screen.getAllByText(/Event/)
expect(eventElements[0]).toHaveTextContent('Event 3')
expect(eventElements[eventElements.length - 1]).toHaveTextContent('Event 1')
})
})
})
Loading