diff --git a/__tests__/components/address-book-settings.test.tsx b/__tests__/components/address-book-settings.test.tsx new file mode 100644 index 0000000..4341db7 --- /dev/null +++ b/__tests__/components/address-book-settings.test.tsx @@ -0,0 +1,58 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { render, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { AddressBookSettings } from '@/components/settings/address-book-settings' +import { addAddressBookEntry } from '@/lib/address-book' + +vi.mock('sonner', () => ({ + toast: { success: vi.fn(), error: vi.fn() }, +})) + +describe('AddressBookSettings', () => { + beforeEach(() => { + localStorage.clear() + }) + + it('renders saved entries', () => { + addAddressBookEntry({ label: 'Alice', address: 'GALICE111' }) + addAddressBookEntry({ label: 'Bob', address: 'GBOB222' }) + + render() + + expect(screen.getByText('Alice')).toBeInTheDocument() + expect(screen.getByText('GALICE111')).toBeInTheDocument() + expect(screen.getByText('Bob')).toBeInTheDocument() + expect(screen.getByText('GBOB222')).toBeInTheDocument() + }) + + it('edit then save updates the entry', async () => { + const user = userEvent.setup() + addAddressBookEntry({ label: 'Alice', address: 'GALICE111' }) + + render() + + await user.click(screen.getByRole('button', { name: 'Edit label' })) + const input = screen.getByDisplayValue('Alice') + await user.clear(input) + await user.type(input, 'Alice Updated') + await user.click(screen.getByRole('button', { name: 'Save' })) + + expect(screen.getByText('Alice Updated')).toBeInTheDocument() + expect(screen.queryByText('Alice')).not.toBeInTheDocument() + }) + + it('delete removes the entry', async () => { + const user = userEvent.setup() + addAddressBookEntry({ label: 'Alice', address: 'GALICE111' }) + + render() + + await user.click(screen.getByRole('button', { name: 'Delete' })) + await user.click(screen.getByRole('button', { name: 'Confirm' })) + + expect(screen.queryByText('Alice')).not.toBeInTheDocument() + expect( + screen.getByText('No saved addresses yet. Addresses you save while creating a stream will appear here.'), + ).toBeInTheDocument() + }) +}) diff --git a/__tests__/components/notification-bell.test.tsx b/__tests__/components/notification-bell.test.tsx new file mode 100644 index 0000000..8a1d007 --- /dev/null +++ b/__tests__/components/notification-bell.test.tsx @@ -0,0 +1,100 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { render, screen, fireEvent } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { NotificationBell } from '@/components/layout/notification-bell' +import { useNotifications } from '@/hooks/use-notifications' + +vi.mock('@/hooks/use-wallet', () => ({ useWallet: vi.fn(() => ({ address: 'GSENDER111' })) })) +vi.mock('@/hooks/use-notifications', () => ({ useNotifications: vi.fn() })) + +const mockedUseNotifications = useNotifications as unknown as ReturnType + +function makeNotification(overrides?: Partial>) { + return { + id: 'notif-1', + type: 'stream_created', + title: 'Stream created', + body: 'A new stream was created for you.', + timestamp: Date.now(), + read: false, + ...overrides, + } +} + +function setNotifications(notifications: ReturnType[]) { + mockedUseNotifications.mockReturnValue({ + notifications, + unreadCount: notifications.filter((n) => !n.read).length, + markAllRead: vi.fn(), + clearAll: vi.fn(), + dismissNotification: vi.fn(), + }) +} + +describe('NotificationBell', () => { + beforeEach(() => { + setNotifications([]) + }) + + it('shows the unread badge count', () => { + setNotifications([makeNotification({ id: 'a' }), makeNotification({ id: 'b', read: true })]) + render() + expect(screen.getByText('1')).toBeInTheDocument() + }) + + it('toggle opens and closes the panel', async () => { + const user = userEvent.setup() + render() + + expect(screen.queryByRole('menu')).not.toBeInTheDocument() + + await user.click(screen.getByTestId('notification-bell-trigger')) + expect(screen.getByRole('menu')).toBeInTheDocument() + + await user.click(screen.getByTestId('notification-bell-trigger')) + expect(screen.queryByRole('menu')).not.toBeInTheDocument() + }) + + it('click outside closes it', async () => { + const user = userEvent.setup() + render( +
+ + +
, + ) + + await user.click(screen.getByTestId('notification-bell-trigger')) + expect(screen.getByRole('menu')).toBeInTheDocument() + + await user.click(screen.getByText('outside')) + expect(screen.queryByRole('menu')).not.toBeInTheDocument() + }) + + it('Escape closes it', async () => { + const user = userEvent.setup() + render() + + await user.click(screen.getByTestId('notification-bell-trigger')) + expect(screen.getByRole('menu')).toBeInTheDocument() + + fireEvent.keyDown(document, { key: 'Escape' }) + expect(screen.queryByRole('menu')).not.toBeInTheDocument() + }) + + it('each notification renders title and body', async () => { + const user = userEvent.setup() + setNotifications([ + makeNotification({ id: 'a', title: 'Stream created', body: 'First stream body' }), + makeNotification({ id: 'b', title: 'Withdrawal', body: 'Second stream body' }), + ]) + render() + + await user.click(screen.getByTestId('notification-bell-trigger')) + + expect(screen.getByText('Stream created')).toBeInTheDocument() + expect(screen.getByText('First stream body')).toBeInTheDocument() + expect(screen.getByText('Withdrawal')).toBeInTheDocument() + expect(screen.getByText('Second stream body')).toBeInTheDocument() + }) +}) diff --git a/__tests__/components/notification-preferences-settings.test.tsx b/__tests__/components/notification-preferences-settings.test.tsx new file mode 100644 index 0000000..48f4a82 --- /dev/null +++ b/__tests__/components/notification-preferences-settings.test.tsx @@ -0,0 +1,41 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { render, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { NotificationPreferencesSettings } from '@/components/notifications/notification-preferences' + +vi.mock('sonner', () => ({ + toast: { warning: vi.fn() }, +})) + +describe('NotificationPreferencesSettings', () => { + beforeEach(() => { + localStorage.clear() + }) + + it('renders a toggle per notification type', () => { + render() + const switches = screen.getAllByRole('switch') + expect(switches).toHaveLength(3) + expect(screen.getByText('Stream created')).toBeInTheDocument() + expect(screen.getByText('Stream cancelled')).toBeInTheDocument() + expect(screen.getByText('Withdrawal')).toBeInTheDocument() + }) + + it('toggling persists via the hook', async () => { + const user = userEvent.setup() + render() + + const toggle = screen.getByRole('switch', { + name: 'Disable Stream created notifications', + }) + expect(toggle).toHaveAttribute('aria-checked', 'true') + + await user.click(toggle) + + expect(toggle).toHaveAttribute('aria-checked', 'false') + expect(toggle).toHaveAccessibleName('Enable Stream created notifications') + + const stored = JSON.parse(localStorage.getItem('flowstar:notification-preferences') || '{}') + expect(stored.stream_created).toBe(false) + }) +}) diff --git a/e2e/landing.spec.ts b/e2e/landing.spec.ts index fabc6c9..cdc9b6e 100644 --- a/e2e/landing.spec.ts +++ b/e2e/landing.spec.ts @@ -3,7 +3,7 @@ import { test, expect } from '@playwright/test' test.describe('Landing page', () => { test('renders hero and navigates to app', async ({ page }) => { await page.goto('/') - await expect(page.locator('text=FlowStar')).toBeVisible() + await expect(page.getByRole('banner').getByText('FlowStar', { exact: true })).toBeVisible() await expect(page.locator('text=Open app').first()).toBeVisible() await page.locator('text=Open app').first().click()