fix unit tests after frontend tests ran
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 16m21s
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 16m21s
This commit is contained in:
@@ -3,15 +3,15 @@ import React from 'react';
|
|||||||
import { screen, waitFor } from '@testing-library/react';
|
import { screen, waitFor } from '@testing-library/react';
|
||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
import Leaderboard from './Leaderboard';
|
import Leaderboard from './Leaderboard';
|
||||||
import * as apiClient from '../services/apiClient';
|
|
||||||
import { LeaderboardUser } from '../types';
|
import { LeaderboardUser } from '../types';
|
||||||
import { createMockLeaderboardUser } from '../tests/utils/mockFactories';
|
import { createMockLeaderboardUser } from '../tests/utils/mockFactories';
|
||||||
import { renderWithProviders } from '../tests/utils/renderWithProviders';
|
import { renderWithProviders } from '../tests/utils/renderWithProviders';
|
||||||
|
import { useLeaderboardQuery } from '../hooks/queries/useLeaderboardQuery';
|
||||||
|
|
||||||
// Must explicitly call vi.mock() for apiClient
|
// Mock the hook directly
|
||||||
vi.mock('../services/apiClient');
|
vi.mock('../hooks/queries/useLeaderboardQuery');
|
||||||
|
|
||||||
const mockedApiClient = vi.mocked(apiClient);
|
const mockedUseLeaderboardQuery = vi.mocked(useLeaderboardQuery);
|
||||||
|
|
||||||
// Mock lucide-react icons to prevent rendering errors in the test environment
|
// Mock lucide-react icons to prevent rendering errors in the test environment
|
||||||
vi.mock('lucide-react', () => ({
|
vi.mock('lucide-react', () => ({
|
||||||
@@ -36,29 +36,38 @@ const mockLeaderboardData: LeaderboardUser[] = [
|
|||||||
describe('Leaderboard', () => {
|
describe('Leaderboard', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
|
// Default mock: loading state
|
||||||
|
mockedUseLeaderboardQuery.mockReturnValue({
|
||||||
|
data: [],
|
||||||
|
isLoading: true,
|
||||||
|
error: null,
|
||||||
|
} as any);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should display a loading message initially', () => {
|
it('should display a loading message initially', () => {
|
||||||
// Mock a pending promise that never resolves to keep it in the loading state
|
|
||||||
mockedApiClient.fetchLeaderboard.mockReturnValue(new Promise(() => {}));
|
|
||||||
renderWithProviders(<Leaderboard />);
|
renderWithProviders(<Leaderboard />);
|
||||||
expect(screen.getByText('Loading Leaderboard...')).toBeInTheDocument();
|
expect(screen.getByText('Loading Leaderboard...')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should display an error message if the API call fails', async () => {
|
it('should display an error message if the API call fails', async () => {
|
||||||
mockedApiClient.fetchLeaderboard.mockResolvedValue(new Response(null, { status: 500 }));
|
mockedUseLeaderboardQuery.mockReturnValue({
|
||||||
|
data: [],
|
||||||
|
isLoading: false,
|
||||||
|
error: new Error('Request failed with status 500'),
|
||||||
|
} as any);
|
||||||
renderWithProviders(<Leaderboard />);
|
renderWithProviders(<Leaderboard />);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByRole('alert')).toBeInTheDocument();
|
|
||||||
// The query hook throws an error with the status code when JSON parsing fails
|
|
||||||
expect(screen.getByText('Error: Request failed with status 500')).toBeInTheDocument();
|
expect(screen.getByText('Error: Request failed with status 500')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should display a generic error for unknown error types', async () => {
|
it('should display a generic error for unknown error types', async () => {
|
||||||
// Use an actual Error object since the component displays error.message
|
mockedUseLeaderboardQuery.mockReturnValue({
|
||||||
mockedApiClient.fetchLeaderboard.mockRejectedValue(new Error('A string error'));
|
data: [],
|
||||||
|
isLoading: false,
|
||||||
|
error: new Error('A string error'),
|
||||||
|
} as any);
|
||||||
renderWithProviders(<Leaderboard />);
|
renderWithProviders(<Leaderboard />);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@@ -68,7 +77,11 @@ describe('Leaderboard', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should display a message when the leaderboard is empty', async () => {
|
it('should display a message when the leaderboard is empty', async () => {
|
||||||
mockedApiClient.fetchLeaderboard.mockResolvedValue(new Response(JSON.stringify([])));
|
mockedUseLeaderboardQuery.mockReturnValue({
|
||||||
|
data: [],
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
} as any);
|
||||||
renderWithProviders(<Leaderboard />);
|
renderWithProviders(<Leaderboard />);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@@ -79,9 +92,11 @@ describe('Leaderboard', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should render the leaderboard with user data on successful fetch', async () => {
|
it('should render the leaderboard with user data on successful fetch', async () => {
|
||||||
mockedApiClient.fetchLeaderboard.mockResolvedValue(
|
mockedUseLeaderboardQuery.mockReturnValue({
|
||||||
new Response(JSON.stringify(mockLeaderboardData)),
|
data: mockLeaderboardData,
|
||||||
);
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
} as any);
|
||||||
renderWithProviders(<Leaderboard />);
|
renderWithProviders(<Leaderboard />);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@@ -104,9 +119,11 @@ describe('Leaderboard', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should render the correct rank icons', async () => {
|
it('should render the correct rank icons', async () => {
|
||||||
mockedApiClient.fetchLeaderboard.mockResolvedValue(
|
mockedUseLeaderboardQuery.mockReturnValue({
|
||||||
new Response(JSON.stringify(mockLeaderboardData)),
|
data: mockLeaderboardData,
|
||||||
);
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
} as any);
|
||||||
renderWithProviders(<Leaderboard />);
|
renderWithProviders(<Leaderboard />);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@@ -123,9 +140,11 @@ describe('Leaderboard', () => {
|
|||||||
const dataWithMissingNames: LeaderboardUser[] = [
|
const dataWithMissingNames: LeaderboardUser[] = [
|
||||||
createMockLeaderboardUser({ user_id: 'user-anon', full_name: null, points: 500, rank: '5' }),
|
createMockLeaderboardUser({ user_id: 'user-anon', full_name: null, points: 500, rank: '5' }),
|
||||||
];
|
];
|
||||||
mockedApiClient.fetchLeaderboard.mockResolvedValue(
|
mockedUseLeaderboardQuery.mockReturnValue({
|
||||||
new Response(JSON.stringify(dataWithMissingNames)),
|
data: dataWithMissingNames,
|
||||||
);
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
} as any);
|
||||||
renderWithProviders(<Leaderboard />);
|
renderWithProviders(<Leaderboard />);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { render, screen, waitFor } from '@testing-library/react';
|
|||||||
import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
|
import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
|
||||||
import { PriceHistoryChart } from './PriceHistoryChart';
|
import { PriceHistoryChart } from './PriceHistoryChart';
|
||||||
import { useUserData } from '../../hooks/useUserData';
|
import { useUserData } from '../../hooks/useUserData';
|
||||||
import * as apiClient from '../../services/apiClient';
|
import { usePriceHistoryQuery } from '../../hooks/queries/usePriceHistoryQuery';
|
||||||
import type { MasterGroceryItem, HistoricalPriceDataPoint } from '../../types';
|
import type { MasterGroceryItem, HistoricalPriceDataPoint } from '../../types';
|
||||||
import {
|
import {
|
||||||
createMockMasterGroceryItem,
|
createMockMasterGroceryItem,
|
||||||
@@ -12,13 +12,14 @@ import {
|
|||||||
} from '../../tests/utils/mockFactories';
|
} from '../../tests/utils/mockFactories';
|
||||||
import { QueryWrapper } from '../../tests/utils/renderWithProviders';
|
import { QueryWrapper } from '../../tests/utils/renderWithProviders';
|
||||||
|
|
||||||
// Mock the apiClient
|
|
||||||
vi.mock('../../services/apiClient');
|
|
||||||
|
|
||||||
// Mock the useUserData hook
|
// Mock the useUserData hook
|
||||||
vi.mock('../../hooks/useUserData');
|
vi.mock('../../hooks/useUserData');
|
||||||
const mockedUseUserData = useUserData as Mock;
|
const mockedUseUserData = useUserData as Mock;
|
||||||
|
|
||||||
|
// Mock the usePriceHistoryQuery hook
|
||||||
|
vi.mock('../../hooks/queries/usePriceHistoryQuery');
|
||||||
|
const mockedUsePriceHistoryQuery = usePriceHistoryQuery as Mock;
|
||||||
|
|
||||||
const renderWithQuery = (ui: React.ReactElement) => render(ui, { wrapper: QueryWrapper });
|
const renderWithQuery = (ui: React.ReactElement) => render(ui, { wrapper: QueryWrapper });
|
||||||
|
|
||||||
// Mock the logger
|
// Mock the logger
|
||||||
@@ -108,6 +109,13 @@ describe('PriceHistoryChart', () => {
|
|||||||
isLoading: false,
|
isLoading: false,
|
||||||
error: null,
|
error: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Default mock for usePriceHistoryQuery (empty/loading false)
|
||||||
|
mockedUsePriceHistoryQuery.mockReturnValue({
|
||||||
|
data: [],
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render a placeholder when there are no watched items', () => {
|
it('should render a placeholder when there are no watched items', () => {
|
||||||
@@ -126,13 +134,21 @@ describe('PriceHistoryChart', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should display a loading state while fetching data', () => {
|
it('should display a loading state while fetching data', () => {
|
||||||
vi.mocked(apiClient.fetchHistoricalPriceData).mockReturnValue(new Promise(() => {}));
|
mockedUsePriceHistoryQuery.mockReturnValue({
|
||||||
|
data: [],
|
||||||
|
isLoading: true,
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
renderWithQuery(<PriceHistoryChart />);
|
renderWithQuery(<PriceHistoryChart />);
|
||||||
expect(screen.getByText('Loading Price History...')).toBeInTheDocument();
|
expect(screen.getByText('Loading Price History...')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should display an error message if the API call fails', async () => {
|
it('should display an error message if the API call fails', async () => {
|
||||||
vi.mocked(apiClient.fetchHistoricalPriceData).mockRejectedValue(new Error('API is down'));
|
mockedUsePriceHistoryQuery.mockReturnValue({
|
||||||
|
data: [],
|
||||||
|
isLoading: false,
|
||||||
|
error: new Error('API is down'),
|
||||||
|
});
|
||||||
renderWithQuery(<PriceHistoryChart />);
|
renderWithQuery(<PriceHistoryChart />);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@@ -142,9 +158,11 @@ describe('PriceHistoryChart', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should display a message if no historical data is returned', async () => {
|
it('should display a message if no historical data is returned', async () => {
|
||||||
vi.mocked(apiClient.fetchHistoricalPriceData).mockResolvedValue(
|
mockedUsePriceHistoryQuery.mockReturnValue({
|
||||||
new Response(JSON.stringify([])),
|
data: [],
|
||||||
);
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
renderWithQuery(<PriceHistoryChart />);
|
renderWithQuery(<PriceHistoryChart />);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@@ -157,14 +175,16 @@ describe('PriceHistoryChart', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should render the chart with data on successful fetch', async () => {
|
it('should render the chart with data on successful fetch', async () => {
|
||||||
vi.mocked(apiClient.fetchHistoricalPriceData).mockResolvedValue(
|
mockedUsePriceHistoryQuery.mockReturnValue({
|
||||||
new Response(JSON.stringify(mockPriceHistory)),
|
data: mockPriceHistory,
|
||||||
);
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
renderWithQuery(<PriceHistoryChart />);
|
renderWithQuery(<PriceHistoryChart />);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
// Check that the API was called with the correct item IDs
|
// Check that the hook was called with the correct item IDs
|
||||||
expect(apiClient.fetchHistoricalPriceData).toHaveBeenCalledWith([1, 2]);
|
expect(mockedUsePriceHistoryQuery).toHaveBeenCalledWith([1, 2], true);
|
||||||
|
|
||||||
// Check that the chart components are rendered
|
// Check that the chart components are rendered
|
||||||
expect(screen.getByTestId('responsive-container')).toBeInTheDocument();
|
expect(screen.getByTestId('responsive-container')).toBeInTheDocument();
|
||||||
@@ -188,15 +208,17 @@ describe('PriceHistoryChart', () => {
|
|||||||
isLoading: true, // Test the isLoading state from the useUserData hook
|
isLoading: true, // Test the isLoading state from the useUserData hook
|
||||||
error: null,
|
error: null,
|
||||||
});
|
});
|
||||||
vi.mocked(apiClient.fetchHistoricalPriceData).mockReturnValue(new Promise(() => {}));
|
// Even if price history is loading or not, user data loading takes precedence in UI
|
||||||
renderWithQuery(<PriceHistoryChart />);
|
renderWithQuery(<PriceHistoryChart />);
|
||||||
expect(screen.getByText('Loading Price History...')).toBeInTheDocument();
|
expect(screen.getByText('Loading Price History...')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should clear the chart when the watchlist becomes empty', async () => {
|
it('should clear the chart when the watchlist becomes empty', async () => {
|
||||||
vi.mocked(apiClient.fetchHistoricalPriceData).mockResolvedValue(
|
mockedUsePriceHistoryQuery.mockReturnValue({
|
||||||
new Response(JSON.stringify(mockPriceHistory)),
|
data: mockPriceHistory,
|
||||||
);
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
const { rerender } = renderWithQuery(<PriceHistoryChart />);
|
const { rerender } = renderWithQuery(<PriceHistoryChart />);
|
||||||
|
|
||||||
// Initial render with items
|
// Initial render with items
|
||||||
@@ -225,7 +247,7 @@ describe('PriceHistoryChart', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should filter out items with only one data point', async () => {
|
it('should filter out items with only one data point', async () => {
|
||||||
const dataWithSinglePoint: HistoricalPriceDataPoint[] = [
|
const dataWithSinglePoint = [
|
||||||
createMockHistoricalPriceDataPoint({
|
createMockHistoricalPriceDataPoint({
|
||||||
master_item_id: 1,
|
master_item_id: 1,
|
||||||
summary_date: '2024-10-01',
|
summary_date: '2024-10-01',
|
||||||
@@ -242,9 +264,11 @@ describe('PriceHistoryChart', () => {
|
|||||||
avg_price_in_cents: 350,
|
avg_price_in_cents: 350,
|
||||||
}), // Almond Milk only has one point
|
}), // Almond Milk only has one point
|
||||||
];
|
];
|
||||||
vi.mocked(apiClient.fetchHistoricalPriceData).mockResolvedValue(
|
mockedUsePriceHistoryQuery.mockReturnValue({
|
||||||
new Response(JSON.stringify(dataWithSinglePoint)),
|
data: dataWithSinglePoint,
|
||||||
);
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
renderWithQuery(<PriceHistoryChart />);
|
renderWithQuery(<PriceHistoryChart />);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@@ -254,7 +278,7 @@ describe('PriceHistoryChart', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should process data to only keep the lowest price for a given day', async () => {
|
it('should process data to only keep the lowest price for a given day', async () => {
|
||||||
const dataWithDuplicateDate: HistoricalPriceDataPoint[] = [
|
const dataWithDuplicateDate = [
|
||||||
createMockHistoricalPriceDataPoint({
|
createMockHistoricalPriceDataPoint({
|
||||||
master_item_id: 1,
|
master_item_id: 1,
|
||||||
summary_date: '2024-10-01',
|
summary_date: '2024-10-01',
|
||||||
@@ -271,9 +295,11 @@ describe('PriceHistoryChart', () => {
|
|||||||
avg_price_in_cents: 99,
|
avg_price_in_cents: 99,
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
vi.mocked(apiClient.fetchHistoricalPriceData).mockResolvedValue(
|
mockedUsePriceHistoryQuery.mockReturnValue({
|
||||||
new Response(JSON.stringify(dataWithDuplicateDate)),
|
data: dataWithDuplicateDate,
|
||||||
);
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
renderWithQuery(<PriceHistoryChart />);
|
renderWithQuery(<PriceHistoryChart />);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@@ -288,7 +314,7 @@ describe('PriceHistoryChart', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should filter out data points with a price of zero', async () => {
|
it('should filter out data points with a price of zero', async () => {
|
||||||
const dataWithZeroPrice: HistoricalPriceDataPoint[] = [
|
const dataWithZeroPrice = [
|
||||||
createMockHistoricalPriceDataPoint({
|
createMockHistoricalPriceDataPoint({
|
||||||
master_item_id: 1,
|
master_item_id: 1,
|
||||||
summary_date: '2024-10-01',
|
summary_date: '2024-10-01',
|
||||||
@@ -305,9 +331,11 @@ describe('PriceHistoryChart', () => {
|
|||||||
avg_price_in_cents: 105,
|
avg_price_in_cents: 105,
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
vi.mocked(apiClient.fetchHistoricalPriceData).mockResolvedValue(
|
mockedUsePriceHistoryQuery.mockReturnValue({
|
||||||
new Response(JSON.stringify(dataWithZeroPrice)),
|
data: dataWithZeroPrice,
|
||||||
);
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
renderWithQuery(<PriceHistoryChart />);
|
renderWithQuery(<PriceHistoryChart />);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@@ -330,9 +358,11 @@ describe('PriceHistoryChart', () => {
|
|||||||
{ master_item_id: 1, summary_date: '2024-10-01', avg_price_in_cents: null }, // Missing price
|
{ master_item_id: 1, summary_date: '2024-10-01', avg_price_in_cents: null }, // Missing price
|
||||||
{ master_item_id: 999, summary_date: '2024-10-01', avg_price_in_cents: 100 }, // ID not in watchlist
|
{ master_item_id: 999, summary_date: '2024-10-01', avg_price_in_cents: 100 }, // ID not in watchlist
|
||||||
];
|
];
|
||||||
vi.mocked(apiClient.fetchHistoricalPriceData).mockResolvedValue(
|
mockedUsePriceHistoryQuery.mockReturnValue({
|
||||||
new Response(JSON.stringify(malformedData)),
|
data: malformedData,
|
||||||
);
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
renderWithQuery(<PriceHistoryChart />);
|
renderWithQuery(<PriceHistoryChart />);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@@ -346,7 +376,7 @@ describe('PriceHistoryChart', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should ignore higher prices for the same day', async () => {
|
it('should ignore higher prices for the same day', async () => {
|
||||||
const dataWithHigherPrice: HistoricalPriceDataPoint[] = [
|
const dataWithHigherPrice = [
|
||||||
createMockHistoricalPriceDataPoint({
|
createMockHistoricalPriceDataPoint({
|
||||||
master_item_id: 1,
|
master_item_id: 1,
|
||||||
summary_date: '2024-10-01',
|
summary_date: '2024-10-01',
|
||||||
@@ -363,9 +393,11 @@ describe('PriceHistoryChart', () => {
|
|||||||
avg_price_in_cents: 100,
|
avg_price_in_cents: 100,
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
vi.mocked(apiClient.fetchHistoricalPriceData).mockResolvedValue(
|
mockedUsePriceHistoryQuery.mockReturnValue({
|
||||||
new Response(JSON.stringify(dataWithHigherPrice)),
|
data: dataWithHigherPrice,
|
||||||
);
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
renderWithQuery(<PriceHistoryChart />);
|
renderWithQuery(<PriceHistoryChart />);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@@ -377,8 +409,11 @@ describe('PriceHistoryChart', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should handle non-Error objects thrown during fetch', async () => {
|
it('should handle non-Error objects thrown during fetch', async () => {
|
||||||
// Use an actual Error object since the component displays error.message
|
mockedUsePriceHistoryQuery.mockReturnValue({
|
||||||
vi.mocked(apiClient.fetchHistoricalPriceData).mockRejectedValue(new Error('Fetch failed'));
|
data: [],
|
||||||
|
isLoading: false,
|
||||||
|
error: new Error('Fetch failed'),
|
||||||
|
});
|
||||||
renderWithQuery(<PriceHistoryChart />);
|
renderWithQuery(<PriceHistoryChart />);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
// src/hooks/useActiveDeals.test.tsx
|
import { renderHook, waitFor } from '@testing-library/react';
|
||||||
import { renderHook, waitFor, act } from '@testing-library/react';
|
|
||||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||||
import { useActiveDeals } from './useActiveDeals';
|
import { useActiveDeals } from './useActiveDeals';
|
||||||
import * as apiClient from '../services/apiClient';
|
|
||||||
import type { Flyer, MasterGroceryItem, FlyerItem } from '../types';
|
import type { Flyer, MasterGroceryItem, FlyerItem } from '../types';
|
||||||
import {
|
import {
|
||||||
createMockFlyer,
|
createMockFlyer,
|
||||||
@@ -12,9 +10,8 @@ import {
|
|||||||
} from '../tests/utils/mockFactories';
|
} from '../tests/utils/mockFactories';
|
||||||
import { mockUseFlyers, mockUseUserData } from '../tests/setup/mockHooks';
|
import { mockUseFlyers, mockUseUserData } from '../tests/setup/mockHooks';
|
||||||
import { QueryWrapper } from '../tests/utils/renderWithProviders';
|
import { QueryWrapper } from '../tests/utils/renderWithProviders';
|
||||||
|
import { useFlyerItemsForFlyersQuery } from './queries/useFlyerItemsForFlyersQuery';
|
||||||
// Must explicitly call vi.mock() for apiClient
|
import { useFlyerItemCountQuery } from './queries/useFlyerItemCountQuery';
|
||||||
vi.mock('../services/apiClient');
|
|
||||||
|
|
||||||
// Mock the hooks to avoid Missing Context errors
|
// Mock the hooks to avoid Missing Context errors
|
||||||
vi.mock('./useFlyers', () => ({
|
vi.mock('./useFlyers', () => ({
|
||||||
@@ -25,7 +22,12 @@ vi.mock('../hooks/useUserData', () => ({
|
|||||||
useUserData: () => mockUseUserData(),
|
useUserData: () => mockUseUserData(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const mockedApiClient = vi.mocked(apiClient);
|
// Mock the query hooks
|
||||||
|
vi.mock('./queries/useFlyerItemsForFlyersQuery');
|
||||||
|
vi.mock('./queries/useFlyerItemCountQuery');
|
||||||
|
|
||||||
|
const mockedUseFlyerItemsForFlyersQuery = vi.mocked(useFlyerItemsForFlyersQuery);
|
||||||
|
const mockedUseFlyerItemCountQuery = vi.mocked(useFlyerItemCountQuery);
|
||||||
|
|
||||||
// Set a consistent "today" for testing flyer validity to make tests deterministic
|
// Set a consistent "today" for testing flyer validity to make tests deterministic
|
||||||
const TODAY = new Date('2024-01-15T12:00:00.000Z');
|
const TODAY = new Date('2024-01-15T12:00:00.000Z');
|
||||||
@@ -33,9 +35,6 @@ const TODAY = new Date('2024-01-15T12:00:00.000Z');
|
|||||||
describe('useActiveDeals Hook', () => {
|
describe('useActiveDeals Hook', () => {
|
||||||
// Use fake timers to control the current date in tests
|
// Use fake timers to control the current date in tests
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// FIX: Only fake the 'Date' object.
|
|
||||||
// This allows `new Date()` to be mocked (via setSystemTime) while keeping
|
|
||||||
// `setTimeout`/`setInterval` native so `waitFor` doesn't hang.
|
|
||||||
vi.useFakeTimers({ toFake: ['Date'] });
|
vi.useFakeTimers({ toFake: ['Date'] });
|
||||||
vi.setSystemTime(TODAY);
|
vi.setSystemTime(TODAY);
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
@@ -58,6 +57,18 @@ describe('useActiveDeals Hook', () => {
|
|||||||
isLoading: false,
|
isLoading: false,
|
||||||
error: null,
|
error: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Default mocks for query hooks
|
||||||
|
mockedUseFlyerItemsForFlyersQuery.mockReturnValue({
|
||||||
|
data: [],
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
} as any);
|
||||||
|
mockedUseFlyerItemCountQuery.mockReturnValue({
|
||||||
|
data: { count: 0 },
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
} as any);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -124,20 +135,18 @@ describe('useActiveDeals Hook', () => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
it('should return loading state initially and then calculated data', async () => {
|
it('should return loading state initially and then calculated data', async () => {
|
||||||
mockedApiClient.countFlyerItemsForFlyers.mockResolvedValue(
|
mockedUseFlyerItemCountQuery.mockReturnValue({
|
||||||
new Response(JSON.stringify({ count: 10 })),
|
data: { count: 10 },
|
||||||
);
|
isLoading: false,
|
||||||
mockedApiClient.fetchFlyerItemsForFlyers.mockResolvedValue(
|
error: null,
|
||||||
new Response(JSON.stringify(mockFlyerItems)),
|
} as any);
|
||||||
);
|
mockedUseFlyerItemsForFlyersQuery.mockReturnValue({
|
||||||
|
data: mockFlyerItems,
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
} as any);
|
||||||
|
|
||||||
const { result } = renderHook(() => useActiveDeals(), { wrapper: QueryWrapper });
|
const { result } = renderHook(() => useActiveDeals(), { wrapper: QueryWrapper });
|
||||||
|
|
||||||
// The hook runs the effect almost immediately. We shouldn't strictly assert false
|
|
||||||
// because depending on render timing, it might already be true.
|
|
||||||
// We mainly care that it eventually resolves.
|
|
||||||
|
|
||||||
// Wait for the hook's useEffect to run and complete
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(result.current.isLoading).toBe(false);
|
expect(result.current.isLoading).toBe(false);
|
||||||
expect(result.current.totalActiveItems).toBe(10);
|
expect(result.current.totalActiveItems).toBe(10);
|
||||||
@@ -147,25 +156,18 @@ describe('useActiveDeals Hook', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should correctly filter for valid flyers and make API calls with their IDs', async () => {
|
it('should correctly filter for valid flyers and make API calls with their IDs', async () => {
|
||||||
mockedApiClient.countFlyerItemsForFlyers.mockResolvedValue(
|
|
||||||
new Response(JSON.stringify({ count: 0 })),
|
|
||||||
);
|
|
||||||
mockedApiClient.fetchFlyerItemsForFlyers.mockResolvedValue(new Response(JSON.stringify([])));
|
|
||||||
|
|
||||||
const { result } = renderHook(() => useActiveDeals(), { wrapper: QueryWrapper });
|
const { result } = renderHook(() => useActiveDeals(), { wrapper: QueryWrapper });
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
// Only the valid flyer (id: 1) should be used in the API calls
|
// Only the valid flyer (id: 1) should be used in the API calls
|
||||||
expect(mockedApiClient.countFlyerItemsForFlyers).toHaveBeenCalledWith([1]);
|
// The second argument is `enabled` which should be true
|
||||||
expect(mockedApiClient.fetchFlyerItemsForFlyers).toHaveBeenCalledWith([1]);
|
expect(mockedUseFlyerItemCountQuery).toHaveBeenCalledWith([1], true);
|
||||||
|
expect(mockedUseFlyerItemsForFlyersQuery).toHaveBeenCalledWith([1], true);
|
||||||
expect(result.current.isLoading).toBe(false);
|
expect(result.current.isLoading).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not fetch flyer items if there are no watched items', async () => {
|
it('should not fetch flyer items if there are no watched items', async () => {
|
||||||
mockedApiClient.countFlyerItemsForFlyers.mockResolvedValue(
|
|
||||||
new Response(JSON.stringify({ count: 10 })),
|
|
||||||
);
|
|
||||||
mockUseUserData.mockReturnValue({
|
mockUseUserData.mockReturnValue({
|
||||||
watchedItems: [],
|
watchedItems: [],
|
||||||
shoppingLists: [],
|
shoppingLists: [],
|
||||||
@@ -173,16 +175,16 @@ describe('useActiveDeals Hook', () => {
|
|||||||
setShoppingLists: vi.fn(),
|
setShoppingLists: vi.fn(),
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
error: null,
|
error: null,
|
||||||
}); // Override for this test
|
});
|
||||||
|
|
||||||
const { result } = renderHook(() => useActiveDeals(), { wrapper: QueryWrapper });
|
const { result } = renderHook(() => useActiveDeals(), { wrapper: QueryWrapper });
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(result.current.isLoading).toBe(false);
|
expect(result.current.isLoading).toBe(false);
|
||||||
expect(result.current.totalActiveItems).toBe(10);
|
// The enabled flag (2nd arg) should be false for items query
|
||||||
expect(result.current.activeDeals).toEqual([]);
|
expect(mockedUseFlyerItemsForFlyersQuery).toHaveBeenCalledWith([1], false);
|
||||||
// The key assertion: fetchFlyerItemsForFlyers should not be called
|
// Count query should still be enabled if there are valid flyers
|
||||||
expect(mockedApiClient.fetchFlyerItemsForFlyers).not.toHaveBeenCalled();
|
expect(mockedUseFlyerItemCountQuery).toHaveBeenCalledWith([1], true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -204,16 +206,20 @@ describe('useActiveDeals Hook', () => {
|
|||||||
expect(result.current.totalActiveItems).toBe(0);
|
expect(result.current.totalActiveItems).toBe(0);
|
||||||
expect(result.current.activeDeals).toEqual([]);
|
expect(result.current.activeDeals).toEqual([]);
|
||||||
// No API calls should be made if there are no valid flyers
|
// No API calls should be made if there are no valid flyers
|
||||||
expect(mockedApiClient.countFlyerItemsForFlyers).not.toHaveBeenCalled();
|
// API calls should be made with empty array, or enabled=false depending on implementation
|
||||||
expect(mockedApiClient.fetchFlyerItemsForFlyers).not.toHaveBeenCalled();
|
// In useActiveDeals.tsx: validFlyerIds.length > 0 is the condition
|
||||||
|
expect(mockedUseFlyerItemCountQuery).toHaveBeenCalledWith([], false);
|
||||||
|
expect(mockedUseFlyerItemsForFlyersQuery).toHaveBeenCalledWith([], false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set an error state if counting items fails', async () => {
|
it('should set an error state if counting items fails', async () => {
|
||||||
const apiError = new Error('Network Failure');
|
const apiError = new Error('Network Failure');
|
||||||
mockedApiClient.countFlyerItemsForFlyers.mockRejectedValue(apiError);
|
mockedUseFlyerItemCountQuery.mockReturnValue({
|
||||||
// Also mock fetchFlyerItemsForFlyers to avoid interference from the other query
|
data: undefined,
|
||||||
mockedApiClient.fetchFlyerItemsForFlyers.mockResolvedValue(new Response(JSON.stringify([])));
|
isLoading: false,
|
||||||
|
error: apiError,
|
||||||
|
} as any);
|
||||||
|
|
||||||
const { result } = renderHook(() => useActiveDeals(), { wrapper: QueryWrapper });
|
const { result } = renderHook(() => useActiveDeals(), { wrapper: QueryWrapper });
|
||||||
|
|
||||||
@@ -225,17 +231,16 @@ describe('useActiveDeals Hook', () => {
|
|||||||
|
|
||||||
it('should set an error state if fetching items fails', async () => {
|
it('should set an error state if fetching items fails', async () => {
|
||||||
const apiError = new Error('Item fetch failed');
|
const apiError = new Error('Item fetch failed');
|
||||||
// Mock the count to succeed but the item fetch to fail
|
mockedUseFlyerItemsForFlyersQuery.mockReturnValue({
|
||||||
mockedApiClient.countFlyerItemsForFlyers.mockResolvedValue(
|
data: undefined,
|
||||||
new Response(JSON.stringify({ count: 10 })),
|
isLoading: false,
|
||||||
);
|
error: apiError,
|
||||||
mockedApiClient.fetchFlyerItemsForFlyers.mockRejectedValue(apiError);
|
} as any);
|
||||||
|
|
||||||
const { result } = renderHook(() => useActiveDeals(), { wrapper: QueryWrapper });
|
const { result } = renderHook(() => useActiveDeals(), { wrapper: QueryWrapper });
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(result.current.isLoading).toBe(false);
|
expect(result.current.isLoading).toBe(false);
|
||||||
// This covers the `|| errorItems?.message` part of the error logic
|
|
||||||
expect(result.current.error).toBe(
|
expect(result.current.error).toBe(
|
||||||
'Could not fetch active deals or totals: Item fetch failed',
|
'Could not fetch active deals or totals: Item fetch failed',
|
||||||
);
|
);
|
||||||
@@ -243,12 +248,16 @@ describe('useActiveDeals Hook', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should correctly map flyer items to DealItem format', async () => {
|
it('should correctly map flyer items to DealItem format', async () => {
|
||||||
mockedApiClient.countFlyerItemsForFlyers.mockResolvedValue(
|
mockedUseFlyerItemCountQuery.mockReturnValue({
|
||||||
new Response(JSON.stringify({ count: 10 })),
|
data: { count: 10 },
|
||||||
);
|
isLoading: false,
|
||||||
mockedApiClient.fetchFlyerItemsForFlyers.mockResolvedValue(
|
error: null,
|
||||||
new Response(JSON.stringify(mockFlyerItems)),
|
} as any);
|
||||||
);
|
mockedUseFlyerItemsForFlyersQuery.mockReturnValue({
|
||||||
|
data: mockFlyerItems,
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
} as any);
|
||||||
|
|
||||||
const { result } = renderHook(() => useActiveDeals(), { wrapper: QueryWrapper });
|
const { result } = renderHook(() => useActiveDeals(), { wrapper: QueryWrapper });
|
||||||
|
|
||||||
@@ -261,7 +270,7 @@ describe('useActiveDeals Hook', () => {
|
|||||||
quantity: 'lb',
|
quantity: 'lb',
|
||||||
storeName: 'Valid Store',
|
storeName: 'Valid Store',
|
||||||
master_item_name: 'Apples',
|
master_item_name: 'Apples',
|
||||||
unit_price: null, // Expect null as the hook ensures undefined is converted to null
|
unit_price: null,
|
||||||
});
|
});
|
||||||
expect(deal).toEqual(expectedDeal);
|
expect(deal).toEqual(expectedDeal);
|
||||||
});
|
});
|
||||||
@@ -276,7 +285,7 @@ describe('useActiveDeals Hook', () => {
|
|||||||
valid_from: '2024-01-10',
|
valid_from: '2024-01-10',
|
||||||
valid_to: '2024-01-20',
|
valid_to: '2024-01-20',
|
||||||
});
|
});
|
||||||
(flyerWithoutStore as any).store = null; // Explicitly set to null
|
(flyerWithoutStore as any).store = null;
|
||||||
|
|
||||||
const itemInFlyerWithoutStore = createMockFlyerItem({
|
const itemInFlyerWithoutStore = createMockFlyerItem({
|
||||||
flyer_item_id: 3,
|
flyer_item_id: 3,
|
||||||
@@ -289,27 +298,21 @@ describe('useActiveDeals Hook', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
mockUseFlyers.mockReturnValue({ ...mockUseFlyers(), flyers: [flyerWithoutStore] });
|
mockUseFlyers.mockReturnValue({ ...mockUseFlyers(), flyers: [flyerWithoutStore] });
|
||||||
mockedApiClient.countFlyerItemsForFlyers.mockResolvedValue(
|
mockedUseFlyerItemsForFlyersQuery.mockReturnValue({
|
||||||
new Response(JSON.stringify({ count: 1 })),
|
data: [itemInFlyerWithoutStore],
|
||||||
);
|
isLoading: false,
|
||||||
mockedApiClient.fetchFlyerItemsForFlyers.mockResolvedValue(
|
error: null,
|
||||||
new Response(JSON.stringify([itemInFlyerWithoutStore])),
|
} as any);
|
||||||
);
|
|
||||||
|
|
||||||
const { result } = renderHook(() => useActiveDeals(), { wrapper: QueryWrapper });
|
const { result } = renderHook(() => useActiveDeals(), { wrapper: QueryWrapper });
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(result.current.activeDeals).toHaveLength(1);
|
expect(result.current.activeDeals).toHaveLength(1);
|
||||||
// This covers the `|| 'Unknown Store'` fallback logic
|
|
||||||
expect(result.current.activeDeals[0].storeName).toBe('Unknown Store');
|
expect(result.current.activeDeals[0].storeName).toBe('Unknown Store');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should filter out items that do not match watched items or have no master ID', async () => {
|
it('should filter out items that do not match watched items or have no master ID', async () => {
|
||||||
mockedApiClient.countFlyerItemsForFlyers.mockResolvedValue(
|
|
||||||
new Response(JSON.stringify({ count: 5 })),
|
|
||||||
);
|
|
||||||
|
|
||||||
const mixedItems: FlyerItem[] = [
|
const mixedItems: FlyerItem[] = [
|
||||||
// Watched item (Master ID 101 is in mockWatchedItems)
|
// Watched item (Master ID 101 is in mockWatchedItems)
|
||||||
createMockFlyerItem({
|
createMockFlyerItem({
|
||||||
@@ -345,9 +348,11 @@ describe('useActiveDeals Hook', () => {
|
|||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
mockedApiClient.fetchFlyerItemsForFlyers.mockResolvedValue(
|
mockedUseFlyerItemsForFlyersQuery.mockReturnValue({
|
||||||
new Response(JSON.stringify(mixedItems)),
|
data: mixedItems,
|
||||||
);
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
} as any);
|
||||||
|
|
||||||
const { result } = renderHook(() => useActiveDeals(), { wrapper: QueryWrapper });
|
const { result } = renderHook(() => useActiveDeals(), { wrapper: QueryWrapper });
|
||||||
|
|
||||||
@@ -360,40 +365,18 @@ describe('useActiveDeals Hook', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return true for isLoading while API calls are pending', async () => {
|
it('should return true for isLoading while API calls are pending', async () => {
|
||||||
// Create promises we can control
|
mockedUseFlyerItemsForFlyersQuery.mockReturnValue({
|
||||||
let resolveCount: (value: Response) => void;
|
data: undefined,
|
||||||
const countPromise = new Promise<Response>((resolve) => {
|
isLoading: true,
|
||||||
resolveCount = resolve;
|
error: null,
|
||||||
});
|
} as any);
|
||||||
|
|
||||||
let resolveItems: (value: Response) => void;
|
|
||||||
const itemsPromise = new Promise<Response>((resolve) => {
|
|
||||||
resolveItems = resolve;
|
|
||||||
});
|
|
||||||
|
|
||||||
mockedApiClient.countFlyerItemsForFlyers.mockReturnValue(countPromise);
|
|
||||||
mockedApiClient.fetchFlyerItemsForFlyers.mockReturnValue(itemsPromise);
|
|
||||||
|
|
||||||
const { result } = renderHook(() => useActiveDeals(), { wrapper: QueryWrapper });
|
const { result } = renderHook(() => useActiveDeals(), { wrapper: QueryWrapper });
|
||||||
|
|
||||||
// Wait for the effect to trigger the API call and set loading to true
|
expect(result.current.isLoading).toBe(true);
|
||||||
await waitFor(() => expect(result.current.isLoading).toBe(true));
|
|
||||||
|
|
||||||
// Resolve promises
|
|
||||||
await act(async () => {
|
|
||||||
resolveCount!(new Response(JSON.stringify({ count: 5 })));
|
|
||||||
resolveItems!(new Response(JSON.stringify([])));
|
|
||||||
});
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(result.current.isLoading).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should re-filter active deals when watched items change (client-side filtering)', async () => {
|
it('should re-filter active deals when watched items change (client-side filtering)', async () => {
|
||||||
// With TanStack Query, changing watchedItems does NOT trigger a new API call
|
|
||||||
// because the query key is based on flyerIds, not watchedItems.
|
|
||||||
// The filtering happens client-side via useMemo. This is more efficient.
|
|
||||||
const allFlyerItems: FlyerItem[] = [
|
const allFlyerItems: FlyerItem[] = [
|
||||||
createMockFlyerItem({
|
createMockFlyerItem({
|
||||||
flyer_item_id: 1,
|
flyer_item_id: 1,
|
||||||
@@ -415,12 +398,11 @@ describe('useActiveDeals Hook', () => {
|
|||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
mockedApiClient.countFlyerItemsForFlyers.mockResolvedValue(
|
mockedUseFlyerItemsForFlyersQuery.mockReturnValue({
|
||||||
new Response(JSON.stringify({ count: 2 })),
|
data: allFlyerItems,
|
||||||
);
|
isLoading: false,
|
||||||
mockedApiClient.fetchFlyerItemsForFlyers.mockResolvedValue(
|
error: null,
|
||||||
new Response(JSON.stringify(allFlyerItems)),
|
} as any);
|
||||||
);
|
|
||||||
|
|
||||||
const { result, rerender } = renderHook(() => useActiveDeals(), { wrapper: QueryWrapper });
|
const { result, rerender } = renderHook(() => useActiveDeals(), { wrapper: QueryWrapper });
|
||||||
|
|
||||||
@@ -433,9 +415,6 @@ describe('useActiveDeals Hook', () => {
|
|||||||
expect(result.current.activeDeals).toHaveLength(1);
|
expect(result.current.activeDeals).toHaveLength(1);
|
||||||
expect(result.current.activeDeals[0].item).toBe('Red Apples');
|
expect(result.current.activeDeals[0].item).toBe('Red Apples');
|
||||||
|
|
||||||
// API should have been called exactly once
|
|
||||||
expect(mockedApiClient.fetchFlyerItemsForFlyers).toHaveBeenCalledTimes(1);
|
|
||||||
|
|
||||||
// Now add Bread to watched items
|
// Now add Bread to watched items
|
||||||
const newWatchedItems = [
|
const newWatchedItems = [
|
||||||
...mockWatchedItems,
|
...mockWatchedItems,
|
||||||
@@ -462,9 +441,6 @@ describe('useActiveDeals Hook', () => {
|
|||||||
const dealItems = result.current.activeDeals.map((d) => d.item);
|
const dealItems = result.current.activeDeals.map((d) => d.item);
|
||||||
expect(dealItems).toContain('Red Apples');
|
expect(dealItems).toContain('Red Apples');
|
||||||
expect(dealItems).toContain('Fresh Bread');
|
expect(dealItems).toContain('Fresh Bread');
|
||||||
|
|
||||||
// The API should NOT be called again - data is already cached
|
|
||||||
expect(mockedApiClient.fetchFlyerItemsForFlyers).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should include flyers valid exactly on the start or end date', async () => {
|
it('should include flyers valid exactly on the start or end date', async () => {
|
||||||
@@ -518,16 +494,10 @@ describe('useActiveDeals Hook', () => {
|
|||||||
refetchFlyers: vi.fn(),
|
refetchFlyers: vi.fn(),
|
||||||
});
|
});
|
||||||
|
|
||||||
mockedApiClient.countFlyerItemsForFlyers.mockResolvedValue(
|
|
||||||
new Response(JSON.stringify({ count: 0 })),
|
|
||||||
);
|
|
||||||
mockedApiClient.fetchFlyerItemsForFlyers.mockResolvedValue(new Response(JSON.stringify([])));
|
|
||||||
|
|
||||||
renderHook(() => useActiveDeals(), { wrapper: QueryWrapper });
|
renderHook(() => useActiveDeals(), { wrapper: QueryWrapper });
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
// Should call with IDs 10, 11, 12. Should NOT include 13.
|
expect(mockedUseFlyerItemCountQuery).toHaveBeenCalledWith([10, 11, 12], true);
|
||||||
expect(mockedApiClient.countFlyerItemsForFlyers).toHaveBeenCalledWith([10, 11, 12]);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -544,12 +514,11 @@ describe('useActiveDeals Hook', () => {
|
|||||||
quantity: undefined,
|
quantity: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
mockedApiClient.countFlyerItemsForFlyers.mockResolvedValue(
|
mockedUseFlyerItemsForFlyersQuery.mockReturnValue({
|
||||||
new Response(JSON.stringify({ count: 1 })),
|
data: [incompleteItem],
|
||||||
);
|
isLoading: false,
|
||||||
mockedApiClient.fetchFlyerItemsForFlyers.mockResolvedValue(
|
error: null,
|
||||||
new Response(JSON.stringify([incompleteItem])),
|
} as any);
|
||||||
);
|
|
||||||
|
|
||||||
const { result } = renderHook(() => useActiveDeals(), { wrapper: QueryWrapper });
|
const { result } = renderHook(() => useActiveDeals(), { wrapper: QueryWrapper });
|
||||||
|
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ describe('useAuth Hook and AuthProvider', () => {
|
|||||||
expect(result.current.userProfile).toBeNull();
|
expect(result.current.userProfile).toBeNull();
|
||||||
expect(mockedTokenStorage.removeToken).toHaveBeenCalled();
|
expect(mockedTokenStorage.removeToken).toHaveBeenCalled();
|
||||||
expect(logger.warn).toHaveBeenCalledWith(
|
expect(logger.warn).toHaveBeenCalledWith(
|
||||||
'[AuthProvider] Token was present but profile is null. Signing out.',
|
'[AuthProvider] Token was present but validation failed. Signing out.',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,15 @@
|
|||||||
// src/pages/MyDealsPage.test.tsx
|
// src/pages/MyDealsPage.test.tsx
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { render, screen, waitFor } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
|
||||||
import MyDealsPage from './MyDealsPage';
|
import MyDealsPage from './MyDealsPage';
|
||||||
import * as apiClient from '../services/apiClient';
|
import { useBestSalePricesQuery } from '../hooks/queries/useBestSalePricesQuery';
|
||||||
import type { WatchedItemDeal } from '../types';
|
import type { WatchedItemDeal } from '../types';
|
||||||
import { createMockWatchedItemDeal } from '../tests/utils/mockFactories';
|
import { createMockWatchedItemDeal } from '../tests/utils/mockFactories';
|
||||||
import { QueryWrapper } from '../tests/utils/renderWithProviders';
|
import { QueryWrapper } from '../tests/utils/renderWithProviders';
|
||||||
|
|
||||||
// Must explicitly call vi.mock() for apiClient
|
vi.mock('../hooks/queries/useBestSalePricesQuery');
|
||||||
vi.mock('../services/apiClient');
|
const mockedUseBestSalePricesQuery = useBestSalePricesQuery as Mock;
|
||||||
|
|
||||||
const mockedApiClient = vi.mocked(apiClient);
|
|
||||||
|
|
||||||
const renderWithQuery = (ui: React.ReactElement) => render(ui, { wrapper: QueryWrapper });
|
const renderWithQuery = (ui: React.ReactElement) => render(ui, { wrapper: QueryWrapper });
|
||||||
|
|
||||||
@@ -26,66 +24,65 @@ vi.mock('lucide-react', () => ({
|
|||||||
describe('MyDealsPage', () => {
|
describe('MyDealsPage', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
|
// Default mock: loading false, empty data
|
||||||
|
mockedUseBestSalePricesQuery.mockReturnValue({
|
||||||
|
data: [],
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should display a loading message initially', () => {
|
it('should display a loading message initially', () => {
|
||||||
// Mock a pending promise
|
mockedUseBestSalePricesQuery.mockReturnValue({
|
||||||
mockedApiClient.fetchBestSalePrices.mockReturnValue(new Promise(() => {}));
|
data: [],
|
||||||
|
isLoading: true,
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
renderWithQuery(<MyDealsPage />);
|
renderWithQuery(<MyDealsPage />);
|
||||||
expect(screen.getByText('Loading your deals...')).toBeInTheDocument();
|
expect(screen.getByText('Loading your deals...')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should display an error message if the API call fails', async () => {
|
it('should display an error message if the API call fails', () => {
|
||||||
mockedApiClient.fetchBestSalePrices.mockResolvedValue(
|
mockedUseBestSalePricesQuery.mockReturnValue({
|
||||||
new Response(null, { status: 500, statusText: 'Server Error' }),
|
data: [],
|
||||||
);
|
isLoading: false,
|
||||||
renderWithQuery(<MyDealsPage />);
|
error: new Error('Request failed with status 500'),
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(screen.getByText('Error')).toBeInTheDocument();
|
|
||||||
// The query hook throws an error with status code when JSON parsing fails on non-ok response
|
|
||||||
expect(screen.getByText('Request failed with status 500')).toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
|
renderWithQuery(<MyDealsPage />);
|
||||||
|
expect(screen.getByText('Error')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Request failed with status 500')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle network errors and log them', async () => {
|
it('should handle network errors and log them', () => {
|
||||||
const networkError = new Error('Network connection failed');
|
mockedUseBestSalePricesQuery.mockReturnValue({
|
||||||
mockedApiClient.fetchBestSalePrices.mockRejectedValue(networkError);
|
data: [],
|
||||||
renderWithQuery(<MyDealsPage />);
|
isLoading: false,
|
||||||
|
error: new Error('Network connection failed'),
|
||||||
await waitFor(() => {
|
|
||||||
expect(screen.getByText('Error')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('Network connection failed')).toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
|
renderWithQuery(<MyDealsPage />);
|
||||||
|
expect(screen.getByText('Error')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Network connection failed')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle unknown errors and log them', async () => {
|
it('should handle unknown errors and log them', () => {
|
||||||
// Mock a rejection with an Error object - TanStack Query passes through Error objects
|
mockedUseBestSalePricesQuery.mockReturnValue({
|
||||||
mockedApiClient.fetchBestSalePrices.mockRejectedValue(new Error('Unknown failure'));
|
data: [],
|
||||||
renderWithQuery(<MyDealsPage />);
|
isLoading: false,
|
||||||
|
error: new Error('Unknown failure'),
|
||||||
await waitFor(() => {
|
|
||||||
expect(screen.getByText('Error')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('Unknown failure')).toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
|
renderWithQuery(<MyDealsPage />);
|
||||||
|
expect(screen.getByText('Error')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Unknown failure')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should display a message when no deals are found', async () => {
|
it('should display a message when no deals are found', () => {
|
||||||
mockedApiClient.fetchBestSalePrices.mockResolvedValue(
|
|
||||||
new Response(JSON.stringify([]), {
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
renderWithQuery(<MyDealsPage />);
|
renderWithQuery(<MyDealsPage />);
|
||||||
|
expect(
|
||||||
await waitFor(() => {
|
screen.getByText('No deals found for your watched items right now.'),
|
||||||
expect(
|
).toBeInTheDocument();
|
||||||
screen.getByText('No deals found for your watched items right now.'),
|
|
||||||
).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render the list of deals on successful fetch', async () => {
|
it('should render the list of deals on successful fetch', () => {
|
||||||
const mockDeals: WatchedItemDeal[] = [
|
const mockDeals: WatchedItemDeal[] = [
|
||||||
createMockWatchedItemDeal({
|
createMockWatchedItemDeal({
|
||||||
master_item_id: 1,
|
master_item_id: 1,
|
||||||
@@ -104,20 +101,18 @@ describe('MyDealsPage', () => {
|
|||||||
valid_to: '2024-10-22',
|
valid_to: '2024-10-22',
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
mockedApiClient.fetchBestSalePrices.mockResolvedValue(
|
mockedUseBestSalePricesQuery.mockReturnValue({
|
||||||
new Response(JSON.stringify(mockDeals), {
|
data: mockDeals,
|
||||||
headers: { 'Content-Type': 'application/json' },
|
isLoading: false,
|
||||||
}),
|
error: null,
|
||||||
);
|
});
|
||||||
|
|
||||||
renderWithQuery(<MyDealsPage />);
|
renderWithQuery(<MyDealsPage />);
|
||||||
|
|
||||||
await waitFor(() => {
|
expect(screen.getByText('Organic Bananas')).toBeInTheDocument();
|
||||||
expect(screen.getByText('Organic Bananas')).toBeInTheDocument();
|
expect(screen.getByText('$0.99')).toBeInTheDocument();
|
||||||
expect(screen.getByText('$0.99')).toBeInTheDocument();
|
expect(screen.getByText('Almond Milk')).toBeInTheDocument();
|
||||||
expect(screen.getByText('Almond Milk')).toBeInTheDocument();
|
expect(screen.getByText('$3.49')).toBeInTheDocument();
|
||||||
expect(screen.getByText('$3.49')).toBeInTheDocument();
|
expect(screen.getByText('Green Grocer')).toBeInTheDocument();
|
||||||
expect(screen.getByText('Green Grocer')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,20 +11,33 @@ import {
|
|||||||
createMockUser,
|
createMockUser,
|
||||||
} from '../tests/utils/mockFactories';
|
} from '../tests/utils/mockFactories';
|
||||||
import { QueryWrapper } from '../tests/utils/renderWithProviders';
|
import { QueryWrapper } from '../tests/utils/renderWithProviders';
|
||||||
|
import { useUserProfileData } from '../hooks/useUserProfileData';
|
||||||
|
|
||||||
// Must explicitly call vi.mock() for apiClient
|
// Must explicitly call vi.mock() for apiClient
|
||||||
vi.mock('../services/apiClient');
|
vi.mock('../services/apiClient');
|
||||||
|
vi.mock('../hooks/useUserProfileData');
|
||||||
|
|
||||||
const renderWithQuery = (ui: React.ReactElement) => render(ui, { wrapper: QueryWrapper });
|
const renderWithQuery = (ui: React.ReactElement) => render(ui, { wrapper: QueryWrapper });
|
||||||
|
|
||||||
const mockedNotificationService = vi.mocked(await import('../services/notificationService'));
|
vi.mock('../services/notificationService', () => ({
|
||||||
|
notifySuccess: vi.fn(),
|
||||||
|
notifyError: vi.fn(),
|
||||||
|
}));
|
||||||
|
import { notifyError } from '../services/notificationService';
|
||||||
|
|
||||||
vi.mock('../components/AchievementsList', () => ({
|
vi.mock('../components/AchievementsList', () => ({
|
||||||
AchievementsList: ({ achievements }: { achievements: (UserAchievement & Achievement)[] }) => (
|
AchievementsList: ({
|
||||||
<div data-testid="achievements-list-mock">Achievements Count: {achievements.length}</div>
|
achievements,
|
||||||
|
}: {
|
||||||
|
achievements: (UserAchievement & Achievement)[] | null;
|
||||||
|
}) => (
|
||||||
|
<div data-testid="achievements-list-mock">Achievements Count: {achievements?.length || 0}</div>
|
||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const mockedApiClient = vi.mocked(apiClient);
|
const mockedApiClient = vi.mocked(apiClient);
|
||||||
|
const mockedUseUserProfileData = vi.mocked(useUserProfileData);
|
||||||
|
const mockedNotifyError = vi.mocked(notifyError);
|
||||||
|
|
||||||
// --- Mock Data ---
|
// --- Mock Data ---
|
||||||
const mockProfile: UserProfile = createMockUserProfile({
|
const mockProfile: UserProfile = createMockUserProfile({
|
||||||
@@ -47,206 +60,109 @@ const mockAchievements: (UserAchievement & Achievement)[] = [
|
|||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const mockSetProfile = vi.fn();
|
||||||
|
|
||||||
describe('UserProfilePage', () => {
|
describe('UserProfilePage', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
|
// Default mock implementation: Success state
|
||||||
|
mockedUseUserProfileData.mockReturnValue({
|
||||||
|
profile: mockProfile,
|
||||||
|
setProfile: mockSetProfile,
|
||||||
|
achievements: mockAchievements,
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ... (Keep existing tests for loading message, error handling, rendering, etc.) ...
|
|
||||||
|
|
||||||
it('should display a loading message initially', () => {
|
it('should display a loading message initially', () => {
|
||||||
mockedApiClient.getAuthenticatedUserProfile.mockReturnValue(new Promise(() => {}));
|
mockedUseUserProfileData.mockReturnValue({
|
||||||
mockedApiClient.getUserAchievements.mockReturnValue(new Promise(() => {}));
|
profile: null,
|
||||||
|
setProfile: mockSetProfile,
|
||||||
|
achievements: [],
|
||||||
|
isLoading: true,
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
renderWithQuery(<UserProfilePage />);
|
renderWithQuery(<UserProfilePage />);
|
||||||
expect(screen.getByText('Loading profile...')).toBeInTheDocument();
|
expect(screen.getByText('Loading profile...')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should display an error message if fetching profile fails', async () => {
|
it('should display an error message if fetching profile fails', () => {
|
||||||
mockedApiClient.getAuthenticatedUserProfile.mockRejectedValue(new Error('Network Error'));
|
mockedUseUserProfileData.mockReturnValue({
|
||||||
mockedApiClient.getUserAchievements.mockResolvedValue(
|
profile: null,
|
||||||
new Response(JSON.stringify(mockAchievements)),
|
setProfile: mockSetProfile,
|
||||||
);
|
achievements: [],
|
||||||
renderWithQuery(<UserProfilePage />);
|
isLoading: false,
|
||||||
|
error: 'Network Error',
|
||||||
await waitFor(() => {
|
|
||||||
expect(screen.getByText('Error: Network Error')).toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
|
renderWithQuery(<UserProfilePage />);
|
||||||
|
expect(screen.getByText('Error: Network Error')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should display an error message if fetching profile returns a non-ok response', async () => {
|
it('should render the profile and achievements on successful fetch', () => {
|
||||||
mockedApiClient.getAuthenticatedUserProfile.mockResolvedValue(
|
|
||||||
new Response(JSON.stringify({ message: 'Auth Failed' }), { status: 401 }),
|
|
||||||
);
|
|
||||||
mockedApiClient.getUserAchievements.mockResolvedValue(
|
|
||||||
new Response(JSON.stringify(mockAchievements)),
|
|
||||||
);
|
|
||||||
renderWithQuery(<UserProfilePage />);
|
renderWithQuery(<UserProfilePage />);
|
||||||
|
expect(screen.getByRole('heading', { name: 'Test User' })).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('test@example.com')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('150 Points')).toBeInTheDocument();
|
||||||
|
expect(screen.getByAltText('User Avatar')).toHaveAttribute('src', mockProfile.avatar_url);
|
||||||
|
expect(screen.getByTestId('achievements-list-mock')).toHaveTextContent('Achievements Count: 1');
|
||||||
|
});
|
||||||
|
|
||||||
await waitFor(() => {
|
it('should render a fallback message if profile is null after loading', () => {
|
||||||
// The query hook parses the error message from the JSON body
|
mockedUseUserProfileData.mockReturnValue({
|
||||||
expect(screen.getByText('Error: Auth Failed')).toBeInTheDocument();
|
profile: null,
|
||||||
|
setProfile: mockSetProfile,
|
||||||
|
achievements: [],
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
it('should display an error message if fetching achievements returns a non-ok response', async () => {
|
|
||||||
mockedApiClient.getAuthenticatedUserProfile.mockResolvedValue(
|
|
||||||
new Response(JSON.stringify(mockProfile)),
|
|
||||||
);
|
|
||||||
mockedApiClient.getUserAchievements.mockResolvedValue(
|
|
||||||
new Response(JSON.stringify({ message: 'Server Busy' }), { status: 503 }),
|
|
||||||
);
|
|
||||||
renderWithQuery(<UserProfilePage />);
|
renderWithQuery(<UserProfilePage />);
|
||||||
|
expect(screen.getByText('Could not load user profile.')).toBeInTheDocument();
|
||||||
await waitFor(() => {
|
|
||||||
// The query hook parses the error message from the JSON body
|
|
||||||
expect(screen.getByText('Error: Server Busy')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should display an error message if fetching achievements fails', async () => {
|
it('should display a fallback avatar if the user has no avatar_url', () => {
|
||||||
mockedApiClient.getAuthenticatedUserProfile.mockResolvedValue(
|
|
||||||
new Response(JSON.stringify(mockProfile)),
|
|
||||||
);
|
|
||||||
mockedApiClient.getUserAchievements.mockRejectedValue(new Error('Achievements service down'));
|
|
||||||
renderWithQuery(<UserProfilePage />);
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(screen.getByText('Error: Achievements service down')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle unknown errors during fetch', async () => {
|
|
||||||
// Use an actual Error object since the hook extracts error.message
|
|
||||||
mockedApiClient.getAuthenticatedUserProfile.mockRejectedValue(new Error('Unknown error'));
|
|
||||||
mockedApiClient.getUserAchievements.mockResolvedValue(
|
|
||||||
new Response(JSON.stringify(mockAchievements)),
|
|
||||||
);
|
|
||||||
renderWithQuery(<UserProfilePage />);
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(screen.getByText('Error: Unknown error')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle null achievements data gracefully on fetch', async () => {
|
|
||||||
mockedApiClient.getAuthenticatedUserProfile.mockResolvedValue(
|
|
||||||
new Response(JSON.stringify(mockProfile)),
|
|
||||||
);
|
|
||||||
// Mock a successful response but with a null body for achievements
|
|
||||||
mockedApiClient.getUserAchievements.mockResolvedValue(new Response(JSON.stringify(null)));
|
|
||||||
renderWithQuery(<UserProfilePage />);
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(screen.getByRole('heading', { name: 'Test User' })).toBeInTheDocument();
|
|
||||||
// The mock achievements list should show 0 achievements because the component
|
|
||||||
// should handle the null response and pass an empty array to the list.
|
|
||||||
expect(screen.getByTestId('achievements-list-mock')).toHaveTextContent(
|
|
||||||
'Achievements Count: 0',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render the profile and achievements on successful fetch', async () => {
|
|
||||||
mockedApiClient.getAuthenticatedUserProfile.mockResolvedValue(
|
|
||||||
new Response(JSON.stringify(mockProfile)),
|
|
||||||
);
|
|
||||||
mockedApiClient.getUserAchievements.mockResolvedValue(
|
|
||||||
new Response(JSON.stringify(mockAchievements)),
|
|
||||||
);
|
|
||||||
renderWithQuery(<UserProfilePage />);
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(screen.getByRole('heading', { name: 'Test User' })).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('test@example.com')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('150 Points')).toBeInTheDocument();
|
|
||||||
expect(screen.getByAltText('User Avatar')).toHaveAttribute('src', mockProfile.avatar_url);
|
|
||||||
expect(screen.getByTestId('achievements-list-mock')).toHaveTextContent(
|
|
||||||
'Achievements Count: 1',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render a fallback message if profile is null after loading', async () => {
|
|
||||||
mockedApiClient.getAuthenticatedUserProfile.mockResolvedValue(
|
|
||||||
new Response(JSON.stringify(null)),
|
|
||||||
);
|
|
||||||
mockedApiClient.getUserAchievements.mockResolvedValue(
|
|
||||||
new Response(JSON.stringify(mockAchievements)),
|
|
||||||
);
|
|
||||||
renderWithQuery(<UserProfilePage />);
|
|
||||||
|
|
||||||
expect(await screen.findByText('Could not load user profile.')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should display a fallback avatar if the user has no avatar_url', async () => {
|
|
||||||
// Create a mock profile with a null avatar_url and a specific name for the seed
|
|
||||||
const profileWithoutAvatar = { ...mockProfile, avatar_url: null, full_name: 'No Avatar User' };
|
const profileWithoutAvatar = { ...mockProfile, avatar_url: null, full_name: 'No Avatar User' };
|
||||||
mockedApiClient.getAuthenticatedUserProfile.mockResolvedValue(
|
mockedUseUserProfileData.mockReturnValue({
|
||||||
new Response(JSON.stringify(profileWithoutAvatar)),
|
profile: profileWithoutAvatar,
|
||||||
);
|
setProfile: mockSetProfile,
|
||||||
mockedApiClient.getUserAchievements.mockResolvedValue(new Response(JSON.stringify([])));
|
achievements: [],
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
|
|
||||||
renderWithQuery(<UserProfilePage />);
|
renderWithQuery(<UserProfilePage />);
|
||||||
|
|
||||||
// Wait for the component to render with the fetched data
|
const avatarImage = screen.getByAltText('User Avatar');
|
||||||
await waitFor(() => {
|
const expectedSrc = 'https://api.dicebear.com/8.x/initials/svg?seed=No Avatar User';
|
||||||
const avatarImage = screen.getByAltText('User Avatar');
|
expect(avatarImage).toHaveAttribute('src', expectedSrc);
|
||||||
// JSDOM might not URL-encode spaces in the src attribute in the same way a browser does.
|
|
||||||
// We adjust the expectation to match the literal string returned by getAttribute.
|
|
||||||
const expectedSrc = 'https://api.dicebear.com/8.x/initials/svg?seed=No Avatar User';
|
|
||||||
console.log('[TEST LOG] Actual Avatar Src:', avatarImage.getAttribute('src'));
|
|
||||||
expect(avatarImage).toHaveAttribute('src', expectedSrc);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should use email for avatar seed if full_name is missing', async () => {
|
it('should use email for avatar seed if full_name is missing', () => {
|
||||||
const profileNoName = { ...mockProfile, full_name: null, avatar_url: null };
|
const profileNoName = { ...mockProfile, full_name: null, avatar_url: null };
|
||||||
mockedApiClient.getAuthenticatedUserProfile.mockResolvedValue(
|
mockedUseUserProfileData.mockReturnValue({
|
||||||
new Response(JSON.stringify(profileNoName)),
|
profile: profileNoName,
|
||||||
);
|
setProfile: mockSetProfile,
|
||||||
mockedApiClient.getUserAchievements.mockResolvedValue(
|
achievements: [],
|
||||||
new Response(JSON.stringify(mockAchievements)),
|
isLoading: false,
|
||||||
);
|
error: null,
|
||||||
|
});
|
||||||
|
|
||||||
renderWithQuery(<UserProfilePage />);
|
renderWithQuery(<UserProfilePage />);
|
||||||
|
|
||||||
await waitFor(() => {
|
const avatar = screen.getByAltText('User Avatar');
|
||||||
const avatar = screen.getByAltText('User Avatar');
|
expect(avatar.getAttribute('src')).toContain(`seed=${profileNoName.user.email}`);
|
||||||
// seed should be the email
|
|
||||||
expect(avatar.getAttribute('src')).toContain(`seed=${profileNoName.user.email}`);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should trigger file input click when avatar is clicked', async () => {
|
it('should trigger file input click when avatar is clicked', () => {
|
||||||
mockedApiClient.getAuthenticatedUserProfile.mockResolvedValue(
|
|
||||||
new Response(JSON.stringify(mockProfile)),
|
|
||||||
);
|
|
||||||
mockedApiClient.getUserAchievements.mockResolvedValue(
|
|
||||||
new Response(JSON.stringify(mockAchievements)),
|
|
||||||
);
|
|
||||||
renderWithQuery(<UserProfilePage />);
|
renderWithQuery(<UserProfilePage />);
|
||||||
|
|
||||||
await screen.findByAltText('User Avatar');
|
|
||||||
|
|
||||||
const fileInput = screen.getByTestId('avatar-file-input');
|
const fileInput = screen.getByTestId('avatar-file-input');
|
||||||
const clickSpy = vi.spyOn(fileInput, 'click');
|
const clickSpy = vi.spyOn(fileInput, 'click');
|
||||||
|
|
||||||
const avatarContainer = screen.getByAltText('User Avatar');
|
const avatarContainer = screen.getByAltText('User Avatar');
|
||||||
fireEvent.click(avatarContainer);
|
fireEvent.click(avatarContainer);
|
||||||
|
|
||||||
expect(clickSpy).toHaveBeenCalled();
|
expect(clickSpy).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Name Editing', () => {
|
describe('Name Editing', () => {
|
||||||
beforeEach(() => {
|
|
||||||
mockedApiClient.getAuthenticatedUserProfile.mockResolvedValue(
|
|
||||||
new Response(JSON.stringify(mockProfile)),
|
|
||||||
);
|
|
||||||
mockedApiClient.getUserAchievements.mockResolvedValue(
|
|
||||||
new Response(JSON.stringify(mockAchievements)),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should allow editing and saving the user name', async () => {
|
it('should allow editing and saving the user name', async () => {
|
||||||
const updatedProfile = { ...mockProfile, full_name: 'Updated Name' };
|
const updatedProfile = { ...mockProfile, full_name: 'Updated Name' };
|
||||||
mockedApiClient.updateUserProfile.mockResolvedValue(
|
mockedApiClient.updateUserProfile.mockResolvedValue(
|
||||||
@@ -254,8 +170,6 @@ describe('UserProfilePage', () => {
|
|||||||
);
|
);
|
||||||
renderWithQuery(<UserProfilePage />);
|
renderWithQuery(<UserProfilePage />);
|
||||||
|
|
||||||
await screen.findByText('Test User');
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: /edit/i }));
|
fireEvent.click(screen.getByRole('button', { name: /edit/i }));
|
||||||
const nameInput = screen.getByRole('textbox');
|
const nameInput = screen.getByRole('textbox');
|
||||||
fireEvent.change(nameInput, { target: { value: 'Updated Name' } });
|
fireEvent.change(nameInput, { target: { value: 'Updated Name' } });
|
||||||
@@ -265,17 +179,14 @@ describe('UserProfilePage', () => {
|
|||||||
expect(mockedApiClient.updateUserProfile).toHaveBeenCalledWith({
|
expect(mockedApiClient.updateUserProfile).toHaveBeenCalledWith({
|
||||||
full_name: 'Updated Name',
|
full_name: 'Updated Name',
|
||||||
});
|
});
|
||||||
expect(screen.getByRole('heading', { name: 'Updated Name' })).toBeInTheDocument();
|
expect(mockSetProfile).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should allow canceling the name edit', async () => {
|
it('should allow canceling the name edit', () => {
|
||||||
renderWithQuery(<UserProfilePage />);
|
renderWithQuery(<UserProfilePage />);
|
||||||
await screen.findByText('Test User');
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: /edit/i }));
|
fireEvent.click(screen.getByRole('button', { name: /edit/i }));
|
||||||
fireEvent.click(screen.getByRole('button', { name: /cancel/i }));
|
fireEvent.click(screen.getByRole('button', { name: /cancel/i }));
|
||||||
|
|
||||||
expect(screen.queryByRole('textbox')).not.toBeInTheDocument();
|
expect(screen.queryByRole('textbox')).not.toBeInTheDocument();
|
||||||
expect(screen.getByRole('heading', { name: 'Test User' })).toBeInTheDocument();
|
expect(screen.getByRole('heading', { name: 'Test User' })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
@@ -285,7 +196,6 @@ describe('UserProfilePage', () => {
|
|||||||
new Response(JSON.stringify({ message: 'Validation failed' }), { status: 400 }),
|
new Response(JSON.stringify({ message: 'Validation failed' }), { status: 400 }),
|
||||||
);
|
);
|
||||||
renderWithQuery(<UserProfilePage />);
|
renderWithQuery(<UserProfilePage />);
|
||||||
await screen.findByText('Test User');
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: /edit/i }));
|
fireEvent.click(screen.getByRole('button', { name: /edit/i }));
|
||||||
const nameInput = screen.getByRole('textbox');
|
const nameInput = screen.getByRole('textbox');
|
||||||
@@ -293,136 +203,33 @@ describe('UserProfilePage', () => {
|
|||||||
fireEvent.click(screen.getByRole('button', { name: /save/i }));
|
fireEvent.click(screen.getByRole('button', { name: /save/i }));
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(mockedNotificationService.notifyError).toHaveBeenCalledWith('Validation failed');
|
expect(mockedNotifyError).toHaveBeenCalledWith('Validation failed');
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should show a default error if saving the name fails with a non-ok response and no message', async () => {
|
|
||||||
mockedApiClient.updateUserProfile.mockResolvedValue(
|
|
||||||
new Response(JSON.stringify({}), { status: 400 }),
|
|
||||||
);
|
|
||||||
renderWithQuery(<UserProfilePage />);
|
|
||||||
await screen.findByText('Test User');
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: /edit/i }));
|
|
||||||
const nameInput = screen.getByRole('textbox');
|
|
||||||
fireEvent.change(nameInput, { target: { value: 'Invalid Name' } });
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: /save/i }));
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
// This covers the `|| 'Failed to update name.'` part of the error throw
|
|
||||||
expect(mockedNotificationService.notifyError).toHaveBeenCalledWith(
|
|
||||||
'Failed to update name.',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle non-ok response with null body when saving name', async () => {
|
|
||||||
// This tests the case where the server returns an error status but an empty/null body.
|
|
||||||
mockedApiClient.updateUserProfile.mockResolvedValue(new Response(null, { status: 500 }));
|
|
||||||
renderWithQuery(<UserProfilePage />);
|
|
||||||
await screen.findByText('Test User');
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: /edit/i }));
|
|
||||||
fireEvent.change(screen.getByRole('textbox'), { target: { value: 'New Name' } });
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: /save/i }));
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
// The component should fall back to the default error message.
|
|
||||||
expect(mockedNotificationService.notifyError).toHaveBeenCalledWith(
|
|
||||||
'Failed to update name.',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle unknown errors when saving name', async () => {
|
|
||||||
mockedApiClient.updateUserProfile.mockRejectedValue('Unknown update error');
|
|
||||||
renderWithQuery(<UserProfilePage />);
|
|
||||||
await screen.findByText('Test User');
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: /edit/i }));
|
|
||||||
const nameInput = screen.getByRole('textbox');
|
|
||||||
fireEvent.change(nameInput, { target: { value: 'New Name' } });
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: /save/i }));
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(mockedNotificationService.notifyError).toHaveBeenCalledWith(
|
|
||||||
'An unknown error occurred.',
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Avatar Upload', () => {
|
describe('Avatar Upload', () => {
|
||||||
beforeEach(() => {
|
it('should upload a new avatar and update the profile', async () => {
|
||||||
mockedApiClient.getAuthenticatedUserProfile.mockResolvedValue(
|
|
||||||
new Response(JSON.stringify(mockProfile)),
|
|
||||||
);
|
|
||||||
mockedApiClient.getUserAchievements.mockResolvedValue(
|
|
||||||
new Response(JSON.stringify(mockAchievements)),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should upload a new avatar and update the image source', async () => {
|
|
||||||
const updatedProfile = { ...mockProfile, avatar_url: 'https://example.com/new-avatar.png' };
|
const updatedProfile = { ...mockProfile, avatar_url: 'https://example.com/new-avatar.png' };
|
||||||
|
mockedApiClient.uploadAvatar.mockResolvedValue(new Response(JSON.stringify(updatedProfile)));
|
||||||
// Log when the mock is called
|
|
||||||
mockedApiClient.uploadAvatar.mockImplementation((file) => {
|
|
||||||
console.log('[TEST LOG] uploadAvatar mock called with:', file.name);
|
|
||||||
// Add a slight delay to ensure "isUploading" state can be observed
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
console.log('[TEST LOG] uploadAvatar mock resolving...');
|
|
||||||
resolve(new Response(JSON.stringify(updatedProfile)));
|
|
||||||
}, 100);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
renderWithQuery(<UserProfilePage />);
|
renderWithQuery(<UserProfilePage />);
|
||||||
|
|
||||||
await screen.findByAltText('User Avatar');
|
|
||||||
|
|
||||||
// Mock the hidden file input
|
|
||||||
const fileInput = screen.getByTestId('avatar-file-input');
|
const fileInput = screen.getByTestId('avatar-file-input');
|
||||||
const file = new File(['(⌐□_□)'], 'chucknorris.png', { type: 'image/png' });
|
const file = new File(['(⌐□_□)'], 'chucknorris.png', { type: 'image/png' });
|
||||||
|
|
||||||
console.log('[TEST LOG] Firing file change event...');
|
|
||||||
fireEvent.change(fileInput, { target: { files: [file] } });
|
fireEvent.change(fileInput, { target: { files: [file] } });
|
||||||
|
|
||||||
// DEBUG: Print current DOM state if spinner is not found immediately
|
|
||||||
// const spinner = screen.queryByTestId('avatar-upload-spinner');
|
|
||||||
// if (!spinner) {
|
|
||||||
// console.log('[TEST LOG] Spinner NOT found immediately after event.');
|
|
||||||
// // screen.debug(); // Uncomment to see DOM
|
|
||||||
// } else {
|
|
||||||
// console.log('[TEST LOG] Spinner FOUND immediately.');
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Wait for the spinner to appear
|
|
||||||
console.log('[TEST LOG] Waiting for spinner...');
|
|
||||||
await screen.findByTestId('avatar-upload-spinner');
|
|
||||||
console.log('[TEST LOG] Spinner found.');
|
|
||||||
|
|
||||||
// Wait for the upload to complete and the UI to update.
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(mockedApiClient.uploadAvatar).toHaveBeenCalledWith(file);
|
expect(mockedApiClient.uploadAvatar).toHaveBeenCalledWith(file);
|
||||||
expect(screen.getByAltText('User Avatar')).toHaveAttribute(
|
expect(mockSetProfile).toHaveBeenCalled();
|
||||||
'src',
|
|
||||||
updatedProfile.avatar_url,
|
|
||||||
);
|
|
||||||
expect(screen.queryByTestId('avatar-upload-spinner')).not.toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not attempt to upload if no file is selected', async () => {
|
it('should not attempt to upload if no file is selected', () => {
|
||||||
renderWithQuery(<UserProfilePage />);
|
renderWithQuery(<UserProfilePage />);
|
||||||
await screen.findByAltText('User Avatar');
|
|
||||||
|
|
||||||
const fileInput = screen.getByTestId('avatar-file-input');
|
const fileInput = screen.getByTestId('avatar-file-input');
|
||||||
// Simulate user canceling the file dialog
|
|
||||||
fireEvent.change(fileInput, { target: { files: null } });
|
fireEvent.change(fileInput, { target: { files: null } });
|
||||||
|
|
||||||
// Assert that no API call was made
|
|
||||||
expect(mockedApiClient.uploadAvatar).not.toHaveBeenCalled();
|
expect(mockedApiClient.uploadAvatar).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -431,96 +238,13 @@ describe('UserProfilePage', () => {
|
|||||||
new Response(JSON.stringify({ message: 'File too large' }), { status: 413 }),
|
new Response(JSON.stringify({ message: 'File too large' }), { status: 413 }),
|
||||||
);
|
);
|
||||||
renderWithQuery(<UserProfilePage />);
|
renderWithQuery(<UserProfilePage />);
|
||||||
await screen.findByAltText('User Avatar');
|
|
||||||
|
|
||||||
const fileInput = screen.getByTestId('avatar-file-input');
|
const fileInput = screen.getByTestId('avatar-file-input');
|
||||||
const file = new File(['(⌐□_□)'], 'large.png', { type: 'image/png' });
|
const file = new File(['(⌐□_□)'], 'large.png', { type: 'image/png' });
|
||||||
fireEvent.change(fileInput, { target: { files: [file] } });
|
fireEvent.change(fileInput, { target: { files: [file] } });
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(mockedNotificationService.notifyError).toHaveBeenCalledWith('File too large');
|
expect(mockedNotifyError).toHaveBeenCalledWith('File too large');
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should show a default error if avatar upload returns a non-ok response and no message', async () => {
|
|
||||||
mockedApiClient.uploadAvatar.mockResolvedValue(
|
|
||||||
new Response(JSON.stringify({}), { status: 413 }),
|
|
||||||
);
|
|
||||||
renderWithQuery(<UserProfilePage />);
|
|
||||||
await screen.findByAltText('User Avatar');
|
|
||||||
|
|
||||||
const fileInput = screen.getByTestId('avatar-file-input');
|
|
||||||
const file = new File(['(⌐□_□)'], 'large.png', { type: 'image/png' });
|
|
||||||
fireEvent.change(fileInput, { target: { files: [file] } });
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
// This covers the `|| 'Failed to upload avatar.'` part of the error throw
|
|
||||||
expect(mockedNotificationService.notifyError).toHaveBeenCalledWith(
|
|
||||||
'Failed to upload avatar.',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle non-ok response with null body when uploading avatar', async () => {
|
|
||||||
mockedApiClient.uploadAvatar.mockResolvedValue(new Response(null, { status: 500 }));
|
|
||||||
renderWithQuery(<UserProfilePage />);
|
|
||||||
await screen.findByAltText('User Avatar');
|
|
||||||
|
|
||||||
const fileInput = screen.getByTestId('avatar-file-input');
|
|
||||||
const file = new File(['(⌐□_□)'], 'chucknorris.png', { type: 'image/png' });
|
|
||||||
fireEvent.change(fileInput, { target: { files: [file] } });
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(mockedNotificationService.notifyError).toHaveBeenCalledWith(
|
|
||||||
'Failed to upload avatar.',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle unknown errors when uploading avatar', async () => {
|
|
||||||
mockedApiClient.uploadAvatar.mockRejectedValue('Unknown upload error');
|
|
||||||
renderWithQuery(<UserProfilePage />);
|
|
||||||
await screen.findByAltText('User Avatar');
|
|
||||||
|
|
||||||
const fileInput = screen.getByTestId('avatar-file-input');
|
|
||||||
const file = new File(['(⌐□_□)'], 'error.png', { type: 'image/png' });
|
|
||||||
fireEvent.change(fileInput, { target: { files: [file] } });
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(mockedNotificationService.notifyError).toHaveBeenCalledWith(
|
|
||||||
'An unknown error occurred.',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should show an error if a non-image file is selected for upload', async () => {
|
|
||||||
// Mock the API client to return a non-OK response, simulating server-side validation failure
|
|
||||||
mockedApiClient.uploadAvatar.mockResolvedValue(
|
|
||||||
new Response(
|
|
||||||
JSON.stringify({
|
|
||||||
message: 'Invalid file type. Only images (png, jpeg, gif) are allowed.',
|
|
||||||
}),
|
|
||||||
{ status: 400, headers: { 'Content-Type': 'application/json' } },
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
renderWithQuery(<UserProfilePage />);
|
|
||||||
await screen.findByAltText('User Avatar');
|
|
||||||
|
|
||||||
const fileInput = screen.getByTestId('avatar-file-input');
|
|
||||||
// Create a mock file that is NOT an image (e.g., a PDF)
|
|
||||||
const nonImageFile = new File(['some text content'], 'document.pdf', {
|
|
||||||
type: 'application/pdf',
|
|
||||||
});
|
|
||||||
|
|
||||||
fireEvent.change(fileInput, { target: { files: [nonImageFile] } });
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(mockedApiClient.uploadAvatar).toHaveBeenCalledWith(nonImageFile);
|
|
||||||
expect(mockedNotificationService.notifyError).toHaveBeenCalledWith(
|
|
||||||
'Invalid file type. Only images (png, jpeg, gif) are allowed.',
|
|
||||||
);
|
|
||||||
expect(screen.queryByTestId('avatar-upload-spinner')).not.toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,14 +5,18 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import { AdminBrandManager } from './AdminBrandManager';
|
import { AdminBrandManager } from './AdminBrandManager';
|
||||||
import * as apiClient from '../../../services/apiClient';
|
import * as apiClient from '../../../services/apiClient';
|
||||||
|
import { useBrandsQuery } from '../../../hooks/queries/useBrandsQuery';
|
||||||
import { createMockBrand } from '../../../tests/utils/mockFactories';
|
import { createMockBrand } from '../../../tests/utils/mockFactories';
|
||||||
import { renderWithProviders } from '../../../tests/utils/renderWithProviders';
|
import { renderWithProviders } from '../../../tests/utils/renderWithProviders';
|
||||||
|
|
||||||
// Must explicitly call vi.mock() for apiClient
|
// Must explicitly call vi.mock() for apiClient and the hook
|
||||||
vi.mock('../../../services/apiClient');
|
vi.mock('../../../services/apiClient');
|
||||||
|
vi.mock('../../../hooks/queries/useBrandsQuery');
|
||||||
|
|
||||||
const mockedApiClient = vi.mocked(apiClient);
|
const mockedApiClient = vi.mocked(apiClient);
|
||||||
|
const mockedUseBrandsQuery = vi.mocked(useBrandsQuery);
|
||||||
const mockedToast = vi.mocked(toast, true);
|
const mockedToast = vi.mocked(toast, true);
|
||||||
|
|
||||||
const mockBrands = [
|
const mockBrands = [
|
||||||
createMockBrand({ brand_id: 1, name: 'No Frills', store_name: 'No Frills', logo_url: null }),
|
createMockBrand({ brand_id: 1, name: 'No Frills', store_name: 'No Frills', logo_url: null }),
|
||||||
createMockBrand({
|
createMockBrand({
|
||||||
@@ -26,70 +30,66 @@ const mockBrands = [
|
|||||||
describe('AdminBrandManager', () => {
|
describe('AdminBrandManager', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
|
// Default mock: loading false, empty data
|
||||||
|
mockedUseBrandsQuery.mockReturnValue({
|
||||||
|
data: [],
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
refetch: vi.fn(),
|
||||||
|
} as any);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render a loading state initially', () => {
|
it('should render a loading state initially', () => {
|
||||||
console.log('TEST START: should render a loading state initially');
|
mockedUseBrandsQuery.mockReturnValue({
|
||||||
// Mock a promise that never resolves to keep the component in a loading state.
|
data: undefined,
|
||||||
console.log('TEST SETUP: Mocking fetchAllBrands with a non-resolving promise.');
|
isLoading: true,
|
||||||
mockedApiClient.fetchAllBrands.mockReturnValue(new Promise(() => {}));
|
error: null,
|
||||||
|
} as any);
|
||||||
|
|
||||||
console.log('TEST ACTION: Rendering AdminBrandManager component.');
|
|
||||||
renderWithProviders(<AdminBrandManager />);
|
renderWithProviders(<AdminBrandManager />);
|
||||||
|
|
||||||
console.log('TEST ASSERTION: Checking for the loading text.');
|
|
||||||
expect(screen.getByText('Loading brands...')).toBeInTheDocument();
|
expect(screen.getByText('Loading brands...')).toBeInTheDocument();
|
||||||
console.log('TEST SUCCESS: Loading text is visible.');
|
|
||||||
console.log('TEST END: should render a loading state initially');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render an error message if fetching brands fails', async () => {
|
it('should render an error message if fetching brands fails', async () => {
|
||||||
console.log('TEST START: should render an error message if fetching brands fails');
|
mockedUseBrandsQuery.mockReturnValue({
|
||||||
const errorMessage = 'Network Error';
|
data: undefined,
|
||||||
console.log(`TEST SETUP: Mocking fetchAllBrands to reject with: ${errorMessage}`);
|
isLoading: false,
|
||||||
mockedApiClient.fetchAllBrands.mockRejectedValue(new Error('Network Error'));
|
error: new Error('Network Error'),
|
||||||
|
} as any);
|
||||||
|
|
||||||
console.log('TEST ACTION: Rendering AdminBrandManager component.');
|
|
||||||
renderWithProviders(<AdminBrandManager />);
|
renderWithProviders(<AdminBrandManager />);
|
||||||
|
|
||||||
console.log('TEST ASSERTION: Waiting for error message to be displayed.');
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByText('Failed to load brands: Network Error')).toBeInTheDocument();
|
expect(screen.getByText('Failed to load brands: Network Error')).toBeInTheDocument();
|
||||||
console.log('TEST SUCCESS: Error message found in the document.');
|
|
||||||
});
|
});
|
||||||
console.log('TEST END: should render an error message if fetching brands fails');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render the list of brands when data is fetched successfully', async () => {
|
it('should render the list of brands when data is fetched successfully', async () => {
|
||||||
console.log('TEST START: should render the list of brands when data is fetched successfully');
|
mockedUseBrandsQuery.mockReturnValue({
|
||||||
// Use mockImplementation to return a new Response object on each call,
|
data: mockBrands,
|
||||||
// preventing "Body has already been read" errors.
|
isLoading: false,
|
||||||
console.log('TEST SETUP: Mocking fetchAllBrands to resolve with mockBrands.');
|
error: null,
|
||||||
mockedApiClient.fetchAllBrands.mockImplementation(
|
} as any);
|
||||||
async () => new Response(JSON.stringify(mockBrands), { status: 200 }),
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log('TEST ACTION: Rendering AdminBrandManager component.');
|
|
||||||
renderWithProviders(<AdminBrandManager />);
|
renderWithProviders(<AdminBrandManager />);
|
||||||
|
|
||||||
console.log('TEST ASSERTION: Waiting for brand list to render.');
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByRole('heading', { name: /brand management/i })).toBeInTheDocument();
|
expect(screen.getByRole('heading', { name: /brand management/i })).toBeInTheDocument();
|
||||||
expect(screen.getByText('No Frills')).toBeInTheDocument();
|
expect(screen.getByText('No Frills')).toBeInTheDocument();
|
||||||
expect(screen.getByText('(Sobeys)')).toBeInTheDocument();
|
expect(screen.getByText('(Sobeys)')).toBeInTheDocument();
|
||||||
expect(screen.getByAltText('Compliments logo')).toBeInTheDocument();
|
expect(screen.getByAltText('Compliments logo')).toBeInTheDocument();
|
||||||
expect(screen.getByText('No Logo')).toBeInTheDocument();
|
expect(screen.getByText('No Logo')).toBeInTheDocument();
|
||||||
console.log('TEST SUCCESS: All brand elements found in the document.');
|
|
||||||
});
|
});
|
||||||
console.log('TEST END: should render the list of brands when data is fetched successfully');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle successful logo upload', async () => {
|
it('should handle successful logo upload', async () => {
|
||||||
console.log('TEST START: should handle successful logo upload');
|
mockedUseBrandsQuery.mockReturnValue({
|
||||||
console.log('TEST SETUP: Mocking fetchAllBrands and uploadBrandLogo for success.');
|
data: mockBrands,
|
||||||
mockedApiClient.fetchAllBrands.mockImplementation(
|
isLoading: false,
|
||||||
async () => new Response(JSON.stringify(mockBrands), { status: 200 }),
|
error: null,
|
||||||
);
|
} as any);
|
||||||
|
|
||||||
mockedApiClient.uploadBrandLogo.mockImplementation(
|
mockedApiClient.uploadBrandLogo.mockImplementation(
|
||||||
async () =>
|
async () =>
|
||||||
new Response(JSON.stringify({ logoUrl: 'https://example.com/new-logo.png' }), {
|
new Response(JSON.stringify({ logoUrl: 'https://example.com/new-logo.png' }), {
|
||||||
@@ -98,41 +98,34 @@ describe('AdminBrandManager', () => {
|
|||||||
);
|
);
|
||||||
mockedToast.loading.mockReturnValue('toast-1');
|
mockedToast.loading.mockReturnValue('toast-1');
|
||||||
|
|
||||||
console.log('TEST ACTION: Rendering AdminBrandManager component.');
|
|
||||||
renderWithProviders(<AdminBrandManager />);
|
renderWithProviders(<AdminBrandManager />);
|
||||||
console.log('TEST ACTION: Waiting for initial brands to render.');
|
|
||||||
await waitFor(() => expect(screen.getByText('No Frills')).toBeInTheDocument());
|
await waitFor(() => expect(screen.getByText('No Frills')).toBeInTheDocument());
|
||||||
|
|
||||||
const file = new File(['logo'], 'logo.png', { type: 'image/png' });
|
const file = new File(['logo'], 'logo.png', { type: 'image/png' });
|
||||||
// Use the new accessible label to find the correct input.
|
|
||||||
const input = screen.getByLabelText('Upload logo for No Frills');
|
const input = screen.getByLabelText('Upload logo for No Frills');
|
||||||
|
|
||||||
console.log('TEST ACTION: Firing file change event on input for "No Frills".');
|
|
||||||
fireEvent.change(input, { target: { files: [file] } });
|
fireEvent.change(input, { target: { files: [file] } });
|
||||||
|
|
||||||
console.log('TEST ASSERTION: Waiting for upload to complete and UI to update.');
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(mockedApiClient.uploadBrandLogo).toHaveBeenCalledWith(1, file);
|
expect(mockedApiClient.uploadBrandLogo).toHaveBeenCalledWith(1, file);
|
||||||
expect(mockedToast.loading).toHaveBeenCalledWith('Uploading logo...');
|
expect(mockedToast.loading).toHaveBeenCalledWith('Uploading logo...');
|
||||||
expect(mockedToast.success).toHaveBeenCalledWith('Logo updated successfully!', {
|
expect(mockedToast.success).toHaveBeenCalledWith('Logo updated successfully!', {
|
||||||
id: 'toast-1',
|
id: 'toast-1',
|
||||||
});
|
});
|
||||||
// Check if the UI updates with the new logo
|
|
||||||
expect(screen.getByAltText('No Frills logo')).toHaveAttribute(
|
expect(screen.getByAltText('No Frills logo')).toHaveAttribute(
|
||||||
'src',
|
'src',
|
||||||
'https://example.com/new-logo.png',
|
'https://example.com/new-logo.png',
|
||||||
);
|
);
|
||||||
console.log('TEST SUCCESS: All assertions for successful upload passed.');
|
|
||||||
});
|
});
|
||||||
console.log('TEST END: should handle successful logo upload');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle failed logo upload with a non-Error object', async () => {
|
it('should handle failed logo upload with a non-Error object', async () => {
|
||||||
console.log('TEST START: should handle failed logo upload with a non-Error object');
|
mockedUseBrandsQuery.mockReturnValue({
|
||||||
mockedApiClient.fetchAllBrands.mockImplementation(
|
data: mockBrands,
|
||||||
async () => new Response(JSON.stringify(mockBrands), { status: 200 }),
|
isLoading: false,
|
||||||
);
|
error: null,
|
||||||
// Reject with a string instead of an Error object to test the fallback error handling
|
} as any);
|
||||||
|
|
||||||
mockedApiClient.uploadBrandLogo.mockRejectedValue('A string error');
|
mockedApiClient.uploadBrandLogo.mockRejectedValue('A string error');
|
||||||
mockedToast.loading.mockReturnValue('toast-non-error');
|
mockedToast.loading.mockReturnValue('toast-non-error');
|
||||||
|
|
||||||
@@ -145,104 +138,88 @@ describe('AdminBrandManager', () => {
|
|||||||
fireEvent.change(input, { target: { files: [file] } });
|
fireEvent.change(input, { target: { files: [file] } });
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
// This assertion verifies that the `String(e)` part of the catch block is executed.
|
|
||||||
expect(mockedToast.error).toHaveBeenCalledWith('Upload failed: A string error', {
|
expect(mockedToast.error).toHaveBeenCalledWith('Upload failed: A string error', {
|
||||||
id: 'toast-non-error',
|
id: 'toast-non-error',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
console.log('TEST END: should handle failed logo upload with a non-Error object');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle failed logo upload', async () => {
|
it('should handle failed logo upload', async () => {
|
||||||
console.log('TEST START: should handle failed logo upload');
|
mockedUseBrandsQuery.mockReturnValue({
|
||||||
console.log('TEST SETUP: Mocking fetchAllBrands for success and uploadBrandLogo for failure.');
|
data: mockBrands,
|
||||||
mockedApiClient.fetchAllBrands.mockImplementation(
|
isLoading: false,
|
||||||
async () => new Response(JSON.stringify(mockBrands), { status: 200 }),
|
error: null,
|
||||||
);
|
} as any);
|
||||||
|
|
||||||
mockedApiClient.uploadBrandLogo.mockRejectedValue(new Error('Upload failed'));
|
mockedApiClient.uploadBrandLogo.mockRejectedValue(new Error('Upload failed'));
|
||||||
mockedToast.loading.mockReturnValue('toast-2');
|
mockedToast.loading.mockReturnValue('toast-2');
|
||||||
|
|
||||||
console.log('TEST ACTION: Rendering AdminBrandManager component.');
|
|
||||||
renderWithProviders(<AdminBrandManager />);
|
renderWithProviders(<AdminBrandManager />);
|
||||||
console.log('TEST ACTION: Waiting for initial brands to render.');
|
|
||||||
await waitFor(() => expect(screen.getByText('No Frills')).toBeInTheDocument());
|
await waitFor(() => expect(screen.getByText('No Frills')).toBeInTheDocument());
|
||||||
|
|
||||||
const file = new File(['logo'], 'logo.png', { type: 'image/png' });
|
const file = new File(['logo'], 'logo.png', { type: 'image/png' });
|
||||||
const input = screen.getByLabelText('Upload logo for No Frills');
|
const input = screen.getByLabelText('Upload logo for No Frills');
|
||||||
|
|
||||||
console.log('TEST ACTION: Firing file change event on input for "No Frills".');
|
|
||||||
fireEvent.change(input, { target: { files: [file] } });
|
fireEvent.change(input, { target: { files: [file] } });
|
||||||
|
|
||||||
console.log('TEST ASSERTION: Waiting for error toast to be called.');
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(mockedToast.error).toHaveBeenCalledWith('Upload failed: Upload failed', {
|
expect(mockedToast.error).toHaveBeenCalledWith('Upload failed: Upload failed', {
|
||||||
id: 'toast-2',
|
id: 'toast-2',
|
||||||
});
|
});
|
||||||
console.log('TEST SUCCESS: Error toast was called with the correct message.');
|
|
||||||
});
|
});
|
||||||
console.log('TEST END: should handle failed logo upload');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show an error toast for invalid file type', async () => {
|
it('should show an error toast for invalid file type', async () => {
|
||||||
console.log('TEST START: should show an error toast for invalid file type');
|
mockedUseBrandsQuery.mockReturnValue({
|
||||||
console.log('TEST SETUP: Mocking fetchAllBrands to resolve successfully.');
|
data: mockBrands,
|
||||||
mockedApiClient.fetchAllBrands.mockImplementation(
|
isLoading: false,
|
||||||
async () => new Response(JSON.stringify(mockBrands), { status: 200 }),
|
error: null,
|
||||||
);
|
} as any);
|
||||||
console.log('TEST ACTION: Rendering AdminBrandManager component.');
|
|
||||||
renderWithProviders(<AdminBrandManager />);
|
renderWithProviders(<AdminBrandManager />);
|
||||||
console.log('TEST ACTION: Waiting for initial brands to render.');
|
|
||||||
await waitFor(() => expect(screen.getByText('No Frills')).toBeInTheDocument());
|
await waitFor(() => expect(screen.getByText('No Frills')).toBeInTheDocument());
|
||||||
|
|
||||||
const file = new File(['text'], 'document.txt', { type: 'text/plain' });
|
const file = new File(['text'], 'document.txt', { type: 'text/plain' });
|
||||||
const input = screen.getByLabelText('Upload logo for No Frills');
|
const input = screen.getByLabelText('Upload logo for No Frills');
|
||||||
|
|
||||||
console.log('TEST ACTION: Firing file change event with invalid file type.');
|
|
||||||
fireEvent.change(input, { target: { files: [file] } });
|
fireEvent.change(input, { target: { files: [file] } });
|
||||||
|
|
||||||
console.log('TEST ASSERTION: Waiting for validation error toast.');
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(mockedToast.error).toHaveBeenCalledWith(
|
expect(mockedToast.error).toHaveBeenCalledWith(
|
||||||
'Invalid file type. Please upload a PNG, JPG, WEBP, or SVG.',
|
'Invalid file type. Please upload a PNG, JPG, WEBP, or SVG.',
|
||||||
);
|
);
|
||||||
expect(mockedApiClient.uploadBrandLogo).not.toHaveBeenCalled();
|
expect(mockedApiClient.uploadBrandLogo).not.toHaveBeenCalled();
|
||||||
console.log('TEST SUCCESS: Validation toast shown and upload API not called.');
|
|
||||||
});
|
});
|
||||||
console.log('TEST END: should show an error toast for invalid file type');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show an error toast for oversized file', async () => {
|
it('should show an error toast for oversized file', async () => {
|
||||||
console.log('TEST START: should show an error toast for oversized file');
|
mockedUseBrandsQuery.mockReturnValue({
|
||||||
console.log('TEST SETUP: Mocking fetchAllBrands to resolve successfully.');
|
data: mockBrands,
|
||||||
mockedApiClient.fetchAllBrands.mockImplementation(
|
isLoading: false,
|
||||||
async () => new Response(JSON.stringify(mockBrands), { status: 200 }),
|
error: null,
|
||||||
);
|
} as any);
|
||||||
console.log('TEST ACTION: Rendering AdminBrandManager component.');
|
|
||||||
renderWithProviders(<AdminBrandManager />);
|
renderWithProviders(<AdminBrandManager />);
|
||||||
console.log('TEST ACTION: Waiting for initial brands to render.');
|
|
||||||
await waitFor(() => expect(screen.getByText('No Frills')).toBeInTheDocument());
|
await waitFor(() => expect(screen.getByText('No Frills')).toBeInTheDocument());
|
||||||
|
|
||||||
const file = new File(['a'.repeat(3 * 1024 * 1024)], 'large.png', { type: 'image/png' });
|
const file = new File(['a'.repeat(3 * 1024 * 1024)], 'large.png', { type: 'image/png' });
|
||||||
const input = screen.getByLabelText('Upload logo for No Frills');
|
const input = screen.getByLabelText('Upload logo for No Frills');
|
||||||
|
|
||||||
console.log('TEST ACTION: Firing file change event with oversized file.');
|
|
||||||
fireEvent.change(input, { target: { files: [file] } });
|
fireEvent.change(input, { target: { files: [file] } });
|
||||||
|
|
||||||
console.log('TEST ASSERTION: Waiting for size validation error toast.');
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(mockedToast.error).toHaveBeenCalledWith('File is too large. Maximum size is 2MB.');
|
expect(mockedToast.error).toHaveBeenCalledWith('File is too large. Maximum size is 2MB.');
|
||||||
expect(mockedApiClient.uploadBrandLogo).not.toHaveBeenCalled();
|
expect(mockedApiClient.uploadBrandLogo).not.toHaveBeenCalled();
|
||||||
console.log('TEST SUCCESS: Size validation toast shown and upload API not called.');
|
|
||||||
});
|
});
|
||||||
console.log('TEST END: should show an error toast for oversized file');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show an error toast if upload fails with a non-ok response', async () => {
|
it('should show an error toast if upload fails with a non-ok response', async () => {
|
||||||
console.log('TEST START: should handle non-ok response from upload API');
|
mockedUseBrandsQuery.mockReturnValue({
|
||||||
mockedApiClient.fetchAllBrands.mockImplementation(
|
data: mockBrands,
|
||||||
async () => new Response(JSON.stringify(mockBrands), { status: 200 }),
|
isLoading: false,
|
||||||
);
|
error: null,
|
||||||
// Mock a failed response (e.g., 400 Bad Request)
|
} as any);
|
||||||
|
|
||||||
mockedApiClient.uploadBrandLogo.mockResolvedValue(
|
mockedApiClient.uploadBrandLogo.mockResolvedValue(
|
||||||
new Response('Invalid image format', { status: 400 }),
|
new Response('Invalid image format', { status: 400 }),
|
||||||
);
|
);
|
||||||
@@ -260,51 +237,49 @@ describe('AdminBrandManager', () => {
|
|||||||
expect(mockedToast.error).toHaveBeenCalledWith('Upload failed: Invalid image format', {
|
expect(mockedToast.error).toHaveBeenCalledWith('Upload failed: Invalid image format', {
|
||||||
id: 'toast-3',
|
id: 'toast-3',
|
||||||
});
|
});
|
||||||
console.log('TEST SUCCESS: Error toast shown for non-ok response.');
|
|
||||||
});
|
});
|
||||||
console.log('TEST END: should handle non-ok response from upload API');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show an error toast if no file is selected', async () => {
|
it('should show an error toast if no file is selected', async () => {
|
||||||
console.log('TEST START: should show an error toast if no file is selected');
|
mockedUseBrandsQuery.mockReturnValue({
|
||||||
console.log('TEST SETUP: Mocking fetchAllBrands to resolve successfully.');
|
data: mockBrands,
|
||||||
mockedApiClient.fetchAllBrands.mockImplementation(
|
isLoading: false,
|
||||||
async () => new Response(JSON.stringify(mockBrands), { status: 200 }),
|
error: null,
|
||||||
);
|
} as any);
|
||||||
|
|
||||||
renderWithProviders(<AdminBrandManager />);
|
renderWithProviders(<AdminBrandManager />);
|
||||||
console.log('TEST ACTION: Waiting for initial brands to render.');
|
|
||||||
await waitFor(() => expect(screen.getByText('No Frills')).toBeInTheDocument());
|
await waitFor(() => expect(screen.getByText('No Frills')).toBeInTheDocument());
|
||||||
|
|
||||||
const input = screen.getByLabelText('Upload logo for No Frills');
|
const input = screen.getByLabelText('Upload logo for No Frills');
|
||||||
// Simulate canceling the file picker by firing a change event with an empty file list.
|
|
||||||
console.log('TEST ACTION: Firing file change event with an empty file list.');
|
|
||||||
fireEvent.change(input, { target: { files: [] } });
|
fireEvent.change(input, { target: { files: [] } });
|
||||||
|
|
||||||
console.log('TEST ASSERTION: Waiting for the "no file selected" error toast.');
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(mockedToast.error).toHaveBeenCalledWith('Please select a file to upload.');
|
expect(mockedToast.error).toHaveBeenCalledWith('Please select a file to upload.');
|
||||||
console.log('TEST SUCCESS: Error toast shown when no file is selected.');
|
|
||||||
});
|
});
|
||||||
console.log('TEST END: should show an error toast if no file is selected');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render an empty table if no brands are found', async () => {
|
it('should render an empty table if no brands are found', async () => {
|
||||||
mockedApiClient.fetchAllBrands.mockImplementation(
|
mockedUseBrandsQuery.mockReturnValue({
|
||||||
async () => new Response(JSON.stringify([]), { status: 200 }),
|
data: [],
|
||||||
);
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
} as any);
|
||||||
|
|
||||||
renderWithProviders(<AdminBrandManager />);
|
renderWithProviders(<AdminBrandManager />);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByRole('heading', { name: /brand management/i })).toBeInTheDocument();
|
expect(screen.getByRole('heading', { name: /brand management/i })).toBeInTheDocument();
|
||||||
// Only the header row should be present
|
|
||||||
expect(screen.getAllByRole('row')).toHaveLength(1);
|
expect(screen.getAllByRole('row')).toHaveLength(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should use status code in error message if response body is empty on upload failure', async () => {
|
it('should use status code in error message if response body is empty on upload failure', async () => {
|
||||||
mockedApiClient.fetchAllBrands.mockImplementation(
|
mockedUseBrandsQuery.mockReturnValue({
|
||||||
async () => new Response(JSON.stringify(mockBrands), { status: 200 }),
|
data: mockBrands,
|
||||||
);
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
} as any);
|
||||||
|
|
||||||
mockedApiClient.uploadBrandLogo.mockImplementation(
|
mockedApiClient.uploadBrandLogo.mockImplementation(
|
||||||
async () => new Response(null, { status: 500, statusText: 'Internal Server Error' }),
|
async () => new Response(null, { status: 500, statusText: 'Internal Server Error' }),
|
||||||
);
|
);
|
||||||
@@ -326,9 +301,12 @@ describe('AdminBrandManager', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should only update the target brand logo and leave others unchanged', async () => {
|
it('should only update the target brand logo and leave others unchanged', async () => {
|
||||||
mockedApiClient.fetchAllBrands.mockImplementation(
|
mockedUseBrandsQuery.mockReturnValue({
|
||||||
async () => new Response(JSON.stringify(mockBrands), { status: 200 }),
|
data: mockBrands,
|
||||||
);
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
} as any);
|
||||||
|
|
||||||
mockedApiClient.uploadBrandLogo.mockImplementation(
|
mockedApiClient.uploadBrandLogo.mockImplementation(
|
||||||
async () => new Response(JSON.stringify({ logoUrl: 'new-logo.png' }), { status: 200 }),
|
async () => new Response(JSON.stringify({ logoUrl: 'new-logo.png' }), { status: 200 }),
|
||||||
);
|
);
|
||||||
@@ -337,17 +315,12 @@ describe('AdminBrandManager', () => {
|
|||||||
renderWithProviders(<AdminBrandManager />);
|
renderWithProviders(<AdminBrandManager />);
|
||||||
await waitFor(() => expect(screen.getByText('No Frills')).toBeInTheDocument());
|
await waitFor(() => expect(screen.getByText('No Frills')).toBeInTheDocument());
|
||||||
|
|
||||||
// Brand 1: No Frills (initially null logo)
|
|
||||||
// Brand 2: Compliments (initially has logo)
|
|
||||||
|
|
||||||
const file = new File(['logo'], 'logo.png', { type: 'image/png' });
|
const file = new File(['logo'], 'logo.png', { type: 'image/png' });
|
||||||
const input = screen.getByLabelText('Upload logo for No Frills'); // Brand 1
|
const input = screen.getByLabelText('Upload logo for No Frills');
|
||||||
fireEvent.change(input, { target: { files: [file] } });
|
fireEvent.change(input, { target: { files: [file] } });
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
// Brand 1 should have new logo
|
|
||||||
expect(screen.getByAltText('No Frills logo')).toHaveAttribute('src', 'new-logo.png');
|
expect(screen.getByAltText('No Frills logo')).toHaveAttribute('src', 'new-logo.png');
|
||||||
// Brand 2 should still have original logo
|
|
||||||
expect(screen.getByAltText('Compliments logo')).toHaveAttribute(
|
expect(screen.getByAltText('Compliments logo')).toHaveAttribute(
|
||||||
'src',
|
'src',
|
||||||
'https://example.com/compliments.png',
|
'https://example.com/compliments.png',
|
||||||
|
|||||||
@@ -20,12 +20,20 @@ describe('rateLimit utils', () => {
|
|||||||
expect(shouldSkipRateLimit(req)).toBe(false);
|
expect(shouldSkipRateLimit(req)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false (do not skip) when NODE_ENV is "development"', async () => {
|
it('should return true (skip) when NODE_ENV is "development"', async () => {
|
||||||
vi.stubEnv('NODE_ENV', 'development');
|
vi.stubEnv('NODE_ENV', 'development');
|
||||||
const { shouldSkipRateLimit } = await import('./rateLimit');
|
const { shouldSkipRateLimit } = await import('./rateLimit');
|
||||||
|
|
||||||
const req = createMockRequest({ headers: {} });
|
const req = createMockRequest({ headers: {} });
|
||||||
expect(shouldSkipRateLimit(req)).toBe(false);
|
expect(shouldSkipRateLimit(req)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true (skip) when NODE_ENV is "staging"', async () => {
|
||||||
|
vi.stubEnv('NODE_ENV', 'staging');
|
||||||
|
const { shouldSkipRateLimit } = await import('./rateLimit');
|
||||||
|
|
||||||
|
const req = createMockRequest({ headers: {} });
|
||||||
|
expect(shouldSkipRateLimit(req)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return true (skip) when NODE_ENV is "test" and header is missing', async () => {
|
it('should return true (skip) when NODE_ENV is "test" and header is missing', async () => {
|
||||||
@@ -55,5 +63,15 @@ describe('rateLimit utils', () => {
|
|||||||
});
|
});
|
||||||
expect(shouldSkipRateLimit(req)).toBe(true);
|
expect(shouldSkipRateLimit(req)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should return false (do not skip) when NODE_ENV is "development" and header is "true"', async () => {
|
||||||
|
vi.stubEnv('NODE_ENV', 'development');
|
||||||
|
const { shouldSkipRateLimit } = await import('./rateLimit');
|
||||||
|
|
||||||
|
const req = createMockRequest({
|
||||||
|
headers: { 'x-test-rate-limit-enable': 'true' },
|
||||||
|
});
|
||||||
|
expect(shouldSkipRateLimit(req)).toBe(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user