import { cleanup, render, screen, within } from '@testing-library/react'; import { afterEach, describe, expect, it, vi } from 'vitest'; import SpoonsPage from '../../src/app/(app)/spoons/page'; const { mockUseQuery, mockPush } = vi.hoisted(() => ({ mockUseQuery: vi.fn(), mockPush: vi.fn(), })); vi.mock('convex/react', () => ({ useConvexAuth: () => ({ isAuthenticated: false }), useMutation: vi.fn(), useQuery: mockUseQuery, })); vi.mock('next/navigation', () => ({ useParams: vi.fn(), useRouter: () => ({ push: mockPush, replace: vi.fn() }), useSearchParams: () => new URLSearchParams(), })); const spoon = { _id: 'spoon-1', name: 'useSend', upstreamOwner: 'acme', upstreamRepo: 'app', forkOwner: 'me', forkRepo: 'app', status: 'active', syncStatus: undefined, syncCadence: 'daily', effectiveUpstreamAheadBy: 0, rawUpstreamAheadBy: 0, forkAheadBy: 0, ignoredUpstreamCount: 0, lastCheckedAt: undefined, }; describe('spoon list accessibility', () => { afterEach(() => { cleanup(); mockUseQuery.mockReset(); mockPush.mockReset(); }); it('does not nest interactive elements inside a role="link" container', () => { mockUseQuery.mockImplementation((_query, args) => { // threads list passes a limit; spoons list passes {}. if (args && 'limit' in args) return []; return [spoon]; }); const { container } = render(); // Invalid ARIA nested-interactive pattern must be gone. expect(container.querySelector('[role="link"]')).toBeNull(); }); it('keeps a primary navigation link and a secondary "Open" link to the spoon detail', () => { mockUseQuery.mockImplementation((_query, args) => { if (args && 'limit' in args) return []; return [spoon]; }); render(); const href = '/spoons/spoon-1'; // Primary link: the spoon name navigates to the detail route. const nameLink = screen.getByRole('link', { name: /useSend/ }); expect(nameLink).toHaveAttribute('href', href); // Secondary action: the "Open" link still exists and points to the detail. const openLink = screen.getByRole('link', { name: /open/i }); expect(openLink).toHaveAttribute('href', href); // Both real anchors point at the destination; no simulated role="link". const links = screen.getAllByRole('link'); for (const link of links.filter((el) => within(el).queryByText('useSend'))) { expect(link).toHaveAttribute('href', href); } }); });