import React from 'react'; import '@testing-library/jest-dom'; import { render, screen, waitFor, fireEvent } from '@testing-library/react'; import { describe, it, expect, vi, beforeEach } from 'vitest'; import FeedList from './FeedList'; import { BrowserRouter } from 'react-router-dom'; describe('FeedList Component', () => { beforeEach(() => { vi.resetAllMocks(); global.fetch = vi.fn(); }); it('renders loading state initially', () => { vi.mocked(global.fetch).mockImplementation(() => new Promise(() => { })); render( { }} setSidebarVisible={() => { }} isMobile={false} /> ); expect(screen.getByText(/loading feeds/i)).toBeInTheDocument(); }); it('renders list of feeds', async () => { const mockFeeds = [ { _id: 1, title: 'Feed One', url: 'http://example.com/rss', web_url: 'http://example.com', category: 'Tech', }, { _id: 2, title: 'Feed Two', url: 'http://test.com/rss', web_url: 'http://test.com', category: 'News', }, ]; vi.mocked(global.fetch).mockImplementation((url) => { const urlStr = url.toString(); if (urlStr.includes('/api/feed/')) { return Promise.resolve({ ok: true, json: async () => mockFeeds, } as Response); } if (urlStr.includes('/api/tag')) { return Promise.resolve({ ok: true, json: async () => [{ title: 'Tech' }], } as Response); } return Promise.reject(new Error(`Unknown URL: ${url}`)); }); render( { }} setSidebarVisible={() => { }} isMobile={false} /> ); await waitFor(() => { expect(screen.queryByText(/loading feeds/i)).not.toBeInTheDocument(); }); // Expand feeds fireEvent.click(screen.getByText(/feeds/i, { selector: 'h4' })); await waitFor(() => { expect(screen.getByText('Feed One')).toBeInTheDocument(); expect(screen.getByText('Feed Two')).toBeInTheDocument(); const techElements = screen.getAllByText('Tech'); expect(techElements.length).toBeGreaterThan(0); }); }); it('handles fetch error', async () => { vi.mocked(global.fetch).mockImplementation(() => Promise.reject(new Error('API Error'))); render( { }} setSidebarVisible={() => { }} isMobile={false} /> ); await waitFor(() => { expect(screen.getByText(/error: api error/i)).toBeInTheDocument(); }); }); it('handles empty feed list', async () => { vi.mocked(global.fetch).mockImplementation((url) => { const urlStr = url.toString(); if (urlStr.includes('/api/feed/')) { return Promise.resolve({ ok: true, json: async () => [], } as Response); } if (urlStr.includes('/api/tag')) { return Promise.resolve({ ok: true, json: async () => [], } as Response); } return Promise.reject(new Error(`Unknown URL: ${url}`)); }); render( { }} setSidebarVisible={() => { }} isMobile={false} /> ); await waitFor(() => { expect(screen.queryByText(/loading feeds/i)).not.toBeInTheDocument(); }); // Expand feeds fireEvent.click(screen.getByText(/feeds/i, { selector: 'h4' })); await waitFor(() => { expect(screen.getByText(/no feeds found/i)).toBeInTheDocument(); }); }); it('handles search submission', async () => { vi.mocked(global.fetch).mockResolvedValue({ ok: true, json: async () => [] } as Response); render( { }} setSidebarVisible={() => { }} isMobile={false} /> ); // Wait for load await waitFor(() => { expect(screen.queryByText(/loading feeds/i)).not.toBeInTheDocument(); }); const searchInput = screen.getByPlaceholderText(/search\.\.\./i); fireEvent.change(searchInput, { target: { value: 'test search' } }); fireEvent.submit(searchInput.closest('form')!); // Should navigate to include search query // Since we're using BrowserRouter in test, we can only check if it doesn't crash // but we can't easily check 'navigate' unless we mock it. }); it('handles logout', async () => { vi.mocked(global.fetch).mockResolvedValue({ ok: true, json: async () => [] } as Response); // Mock window.location const originalLocation = window.location; const locationMock = new URL('http://localhost/v2/'); delete (window as { location?: Location }).location; (window as { location?: unknown }).location = { ...originalLocation, assign: vi.fn(), replace: vi.fn(), get href() { return locationMock.href; }, set href(val: string) { locationMock.href = new URL(val, locationMock.origin).href; } }; render( { }} setSidebarVisible={() => { }} isMobile={false} /> ); // Wait for load await waitFor(() => { expect(screen.queryByText(/loading feeds/i)).not.toBeInTheDocument(); }); const logoutBtn = screen.getByText(/logout/i); fireEvent.click(logoutBtn); await waitFor(() => { expect(global.fetch).toHaveBeenCalledWith('/api/logout', expect.any(Object)); expect(window.location.href).toContain('/v2/login'); }); window.location = originalLocation; }); it('closes sidebar on mobile link click', async () => { vi.mocked(global.fetch).mockResolvedValue({ ok: true, json: async () => [] } as Response); const setSidebarVisible = vi.fn(); render( { }} setSidebarVisible={setSidebarVisible} isMobile={true} /> ); // Wait for load await waitFor(() => { expect(screen.queryByText(/loading feeds/i)).not.toBeInTheDocument(); }); const unreadLink = screen.getByText(/unread/i); fireEvent.click(unreadLink); expect(setSidebarVisible).toHaveBeenCalledWith(false); }); });