Some checks failed
Deploy to Test Environment / deploy-to-test (push) Failing after 1m1s
207 lines
7.5 KiB
TypeScript
207 lines
7.5 KiB
TypeScript
// 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(<RecipeSuggester />);
|
|
|
|
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(<RecipeSuggester />);
|
|
|
|
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(<RecipeSuggester />);
|
|
|
|
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(<RecipeSuggester />);
|
|
|
|
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(<RecipeSuggester />);
|
|
|
|
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(<RecipeSuggester />);
|
|
|
|
// 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(<RecipeSuggester />);
|
|
|
|
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(<RecipeSuggester />);
|
|
|
|
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.',
|
|
);
|
|
});
|
|
});
|