// src/components/RecipeSuggester.test.tsx import { describe, it, expect, vi, beforeEach } from 'vitest'; import { screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { RecipeSuggester } from './RecipeSuggester'; // This should be after mocks import * as apiClient from '../services/apiClient'; import { logger } from '../services/logger.client'; import { renderWithProviders } from '../tests/utils/renderWithProviders'; import '@testing-library/jest-dom'; // Must explicitly call vi.mock() for apiClient vi.mock('../services/apiClient'); const mockedApiClient = vi.mocked(apiClient); describe('RecipeSuggester Component', () => { beforeEach(() => { vi.clearAllMocks(); // Reset console logs if needed, or just keep them for debug visibility }); it('renders correctly with initial state', () => { console.log('TEST: Verifying initial render state'); renderWithProviders(); expect(screen.getByText('Get a Recipe Suggestion')).toBeInTheDocument(); expect(screen.getByLabelText(/Ingredients:/i)).toBeInTheDocument(); expect(screen.getByRole('button', { name: /Suggest a Recipe/i })).toBeInTheDocument(); expect(screen.queryByText('Getting suggestion...')).not.toBeInTheDocument(); }); it('shows validation error if no ingredients are entered', async () => { console.log('TEST: Verifying validation for empty input'); const user = userEvent.setup(); renderWithProviders(); const button = screen.getByRole('button', { name: /Suggest a Recipe/i }); await user.click(button); expect(await screen.findByText('Please enter at least one ingredient.')).toBeInTheDocument(); expect(mockedApiClient.suggestRecipe).not.toHaveBeenCalled(); console.log('TEST: Validation error displayed correctly'); }); it('calls suggestRecipe and displays suggestion on success', async () => { console.log('TEST: Verifying successful recipe suggestion flow'); const user = userEvent.setup(); renderWithProviders(); const input = screen.getByLabelText(/Ingredients:/i); await user.type(input, 'chicken, rice'); // Mock successful API response const mockSuggestion = 'Here is a nice Chicken and Rice recipe...'; // Add a delay to ensure the loading state is visible during the test mockedApiClient.suggestRecipe.mockImplementation(async () => { await new Promise((resolve) => setTimeout(resolve, 50)); return { ok: true, json: async () => ({ suggestion: mockSuggestion }), } as Response; }); const button = screen.getByRole('button', { name: /Suggest a Recipe/i }); await user.click(button); // Check loading state expect(screen.getByRole('button')).toBeDisabled(); expect(screen.getByText('Getting suggestion...')).toBeInTheDocument(); await waitFor(() => { expect(screen.getByText(mockSuggestion)).toBeInTheDocument(); }); expect(mockedApiClient.suggestRecipe).toHaveBeenCalledWith(['chicken', 'rice']); console.log('TEST: Suggestion displayed and API called with correct args'); }); it('handles API errors (non-200 response) gracefully', async () => { console.log('TEST: Verifying API error handling (400/500 responses)'); const user = userEvent.setup(); renderWithProviders(); const input = screen.getByLabelText(/Ingredients:/i); await user.type(input, 'rocks'); // Mock API failure response const errorMessage = 'Invalid ingredients provided.'; mockedApiClient.suggestRecipe.mockResolvedValue({ ok: false, json: async () => ({ message: errorMessage }), } as Response); const button = screen.getByRole('button', { name: /Suggest a Recipe/i }); await user.click(button); await waitFor(() => { expect(screen.getByText(errorMessage)).toBeInTheDocument(); }); // Ensure loading state is reset expect(screen.getByRole('button', { name: /Suggest a Recipe/i })).toBeEnabled(); console.log('TEST: API error message displayed to user'); }); it('handles network exceptions and logs them', async () => { console.log('TEST: Verifying network exception handling'); const user = userEvent.setup(); renderWithProviders(); const input = screen.getByLabelText(/Ingredients:/i); await user.type(input, 'beef'); // Mock network error const networkError = new Error('Network Error'); mockedApiClient.suggestRecipe.mockRejectedValue(networkError); const button = screen.getByRole('button', { name: /Suggest a Recipe/i }); await user.click(button); await waitFor(() => { expect(screen.getByText('Network Error')).toBeInTheDocument(); }); expect(logger.error).toHaveBeenCalledWith( { error: networkError }, 'Failed to fetch recipe suggestion.', ); console.log('TEST: Network error caught and logged'); }); it('clears previous errors when submitting again', async () => { console.log('TEST: Verifying error clearing on re-submit'); const user = userEvent.setup(); renderWithProviders(); // Trigger validation error first const button = screen.getByRole('button', { name: /Suggest a Recipe/i }); await user.click(button); expect(screen.getByText('Please enter at least one ingredient.')).toBeInTheDocument(); // Now type something to clear it (state change doesn't clear it, submit does) const input = screen.getByLabelText(/Ingredients:/i); await user.type(input, 'tofu'); // Mock success for the second click mockedApiClient.suggestRecipe.mockResolvedValue({ ok: true, json: async () => ({ suggestion: 'Tofu Stir Fry' }), } as Response); await user.click(button); await waitFor(() => { expect(screen.queryByText('Please enter at least one ingredient.')).not.toBeInTheDocument(); expect(screen.getByText('Tofu Stir Fry')).toBeInTheDocument(); }); console.log('TEST: Previous error cleared successfully'); }); it('uses default error message when API error response has no message', async () => { console.log('TEST: Verifying default error message for API failure'); const user = userEvent.setup(); renderWithProviders(); const input = screen.getByLabelText(/Ingredients:/i); await user.type(input, 'mystery'); // Mock API failure response without a message property mockedApiClient.suggestRecipe.mockResolvedValue({ ok: false, json: async () => ({}), // Empty object } as Response); const button = screen.getByRole('button', { name: /Suggest a Recipe/i }); await user.click(button); await waitFor(() => { expect(screen.getByText('Failed to get suggestion.')).toBeInTheDocument(); }); }); it('handles non-Error objects thrown during fetch', async () => { console.log('TEST: Verifying handling of non-Error exceptions'); const user = userEvent.setup(); renderWithProviders(); const input = screen.getByLabelText(/Ingredients:/i); await user.type(input, 'chaos'); // Mock a rejection that is NOT an Error object mockedApiClient.suggestRecipe.mockRejectedValue('Something weird happened'); const button = screen.getByRole('button', { name: /Suggest a Recipe/i }); await user.click(button); await waitFor(() => { expect(screen.getByText('An unknown error occurred.')).toBeInTheDocument(); }); expect(logger.error).toHaveBeenCalledWith( { error: 'Something weird happened' }, 'Failed to fetch recipe suggestion.', ); }); });