diff --git a/src/__tests__/ClaimsPage.test.tsx b/src/__tests__/ClaimsPage.test.tsx new file mode 100644 index 0000000..81d9c1b --- /dev/null +++ b/src/__tests__/ClaimsPage.test.tsx @@ -0,0 +1,56 @@ +import { fireEvent, render, screen } from '@testing-library/react'; +import ClaimsPage from '../app/claims/page'; + +const { useWallet, useClaims } = vi.hoisted(() => ({ + useWallet: vi.fn(), + useClaims: vi.fn(), +})); + +vi.mock('@/hooks/useWallet', () => ({ useWallet })); +vi.mock('@/hooks/useClaims', () => ({ useClaims })); +vi.mock('@/components/ClaimHistoryTable', () => ({ ClaimHistoryTable: () => null })); +vi.mock('@/components/Breadcrumb', () => ({ Breadcrumb: () => null })); +vi.mock('@/components/ConnectWalletPrompt', () => ({ ConnectWalletPrompt: () => null })); +vi.mock('@/lib/claimsExport', () => ({ downloadClaimsCSV: vi.fn(), downloadClaimsJSON: vi.fn() })); + +describe('ClaimsPage export dropdown (#546)', () => { + beforeEach(() => { + useWallet.mockReturnValue({ address: 'GWALLET', connected: true }); + useClaims.mockReturnValue({ + claims: [{ id: 'c1' }], + loading: false, + error: null, + pollingError: null, + refetch: vi.fn(), + paused: false, + togglePause: vi.fn(), + secondsUntilRefresh: null, + secondsSinceRefresh: null, + }); + }); + + it('opens a viewport-constrained menu anchored left on mobile', () => { + render(); + const trigger = screen.getByRole('button', { name: /Export/ }); + expect(trigger.getAttribute('aria-expanded')).toBe('false'); + + fireEvent.click(trigger); + + expect(trigger.getAttribute('aria-expanded')).toBe('true'); + const menu = screen.getByRole('menu'); + expect(menu.className).toContain('max-w-[calc(100vw-3rem)]'); + expect(menu.className).toContain('left-0'); + expect(menu.className).toContain('sm:right-0'); + expect(screen.getAllByRole('menuitem')).toHaveLength(2); + }); + + it('closes the menu on Escape', () => { + render(); + fireEvent.click(screen.getByRole('button', { name: /Export/ })); + expect(screen.queryByRole('menu')).not.toBeNull(); + + fireEvent.keyDown(document, { key: 'Escape' }); + + expect(screen.queryByRole('menu')).toBeNull(); + }); +}); diff --git a/src/app/claims/page.tsx b/src/app/claims/page.tsx index 7758376..cc261d3 100644 --- a/src/app/claims/page.tsx +++ b/src/app/claims/page.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState, useCallback } from 'react'; +import { useState, useCallback, useEffect } from 'react'; import { useWallet } from '@/hooks/useWallet'; import { useClaims } from '@/hooks/useClaims'; import { ConnectWalletPrompt } from '@/components/ConnectWalletPrompt'; @@ -26,6 +26,15 @@ export default function ClaimsPage() { const [refreshing, setRefreshing] = useState(false); const [exportOpen, setExportOpen] = useState(false); + useEffect(() => { + if (!exportOpen) return; + const onKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape') setExportOpen(false); + }; + document.addEventListener('keydown', onKeyDown); + return () => document.removeEventListener('keydown', onKeyDown); + }, [exportOpen]); + const handleRefresh = useCallback(async () => { setRefreshing(true); await refetch(); @@ -48,20 +57,22 @@ export default function ClaimsPage() { return (
-
+

Claim History

All claims submitted from your connected wallet

-
-
+
+
{claims.length > 0 && (