All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 33m19s
358 lines
16 KiB
TypeScript
358 lines
16 KiB
TypeScript
// src/pages/admin/components/AdminBrandManager.test.tsx
|
|
import React from 'react';
|
|
import { screen, fireEvent, waitFor } from '@testing-library/react';
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
import toast from 'react-hot-toast';
|
|
import { AdminBrandManager } from './AdminBrandManager';
|
|
import * as apiClient from '../../../services/apiClient';
|
|
import { createMockBrand } from '../../../tests/utils/mockFactories';
|
|
import { renderWithProviders } from '../../../tests/utils/renderWithProviders';
|
|
|
|
// After mocking, we can get a type-safe mocked version of the module.
|
|
// This allows us to use .mockResolvedValue, .mockRejectedValue, etc. on the functions.
|
|
// The apiClient is now mocked globally via src/tests/setup/tests-setup-unit.ts.
|
|
const mockedApiClient = vi.mocked(apiClient);
|
|
const mockedToast = vi.mocked(toast, true);
|
|
const mockBrands = [
|
|
createMockBrand({ brand_id: 1, name: 'No Frills', store_name: 'No Frills', logo_url: null }),
|
|
createMockBrand({
|
|
brand_id: 2,
|
|
name: 'Compliments',
|
|
store_name: 'Sobeys',
|
|
logo_url: 'https://example.com/compliments.png',
|
|
}),
|
|
];
|
|
|
|
describe('AdminBrandManager', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it('should render a loading state initially', () => {
|
|
console.log('TEST START: should render a loading state initially');
|
|
// Mock a promise that never resolves to keep the component in a loading state.
|
|
console.log('TEST SETUP: Mocking fetchAllBrands with a non-resolving promise.');
|
|
mockedApiClient.fetchAllBrands.mockReturnValue(new Promise(() => {}));
|
|
|
|
console.log('TEST ACTION: Rendering AdminBrandManager component.');
|
|
renderWithProviders(<AdminBrandManager />);
|
|
|
|
console.log('TEST ASSERTION: Checking for the loading text.');
|
|
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 () => {
|
|
console.log('TEST START: should render an error message if fetching brands fails');
|
|
const errorMessage = 'Network Error';
|
|
console.log(`TEST SETUP: Mocking fetchAllBrands to reject with: ${errorMessage}`);
|
|
mockedApiClient.fetchAllBrands.mockRejectedValue(new Error('Network Error'));
|
|
|
|
console.log('TEST ACTION: Rendering AdminBrandManager component.');
|
|
renderWithProviders(<AdminBrandManager />);
|
|
|
|
console.log('TEST ASSERTION: Waiting for error message to be displayed.');
|
|
await waitFor(() => {
|
|
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 () => {
|
|
console.log('TEST START: should render the list of brands when data is fetched successfully');
|
|
// Use mockImplementation to return a new Response object on each call,
|
|
// preventing "Body has already been read" errors.
|
|
console.log('TEST SETUP: Mocking fetchAllBrands to resolve with mockBrands.');
|
|
mockedApiClient.fetchAllBrands.mockImplementation(
|
|
async () => new Response(JSON.stringify(mockBrands), { status: 200 }),
|
|
);
|
|
|
|
console.log('TEST ACTION: Rendering AdminBrandManager component.');
|
|
renderWithProviders(<AdminBrandManager />);
|
|
|
|
console.log('TEST ASSERTION: Waiting for brand list to render.');
|
|
await waitFor(() => {
|
|
expect(screen.getByRole('heading', { name: /brand management/i })).toBeInTheDocument();
|
|
expect(screen.getByText('No Frills')).toBeInTheDocument();
|
|
expect(screen.getByText('(Sobeys)')).toBeInTheDocument();
|
|
expect(screen.getByAltText('Compliments 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 () => {
|
|
console.log('TEST START: should handle successful logo upload');
|
|
console.log('TEST SETUP: Mocking fetchAllBrands and uploadBrandLogo for success.');
|
|
mockedApiClient.fetchAllBrands.mockImplementation(
|
|
async () => new Response(JSON.stringify(mockBrands), { status: 200 }),
|
|
);
|
|
mockedApiClient.uploadBrandLogo.mockImplementation(
|
|
async () =>
|
|
new Response(JSON.stringify({ logoUrl: 'https://example.com/new-logo.png' }), {
|
|
status: 200,
|
|
}),
|
|
);
|
|
mockedToast.loading.mockReturnValue('toast-1');
|
|
|
|
console.log('TEST ACTION: Rendering AdminBrandManager component.');
|
|
renderWithProviders(<AdminBrandManager />);
|
|
console.log('TEST ACTION: Waiting for initial brands to render.');
|
|
await waitFor(() => expect(screen.getByText('No Frills')).toBeInTheDocument());
|
|
|
|
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');
|
|
|
|
console.log('TEST ACTION: Firing file change event on input for "No Frills".');
|
|
fireEvent.change(input, { target: { files: [file] } });
|
|
|
|
console.log('TEST ASSERTION: Waiting for upload to complete and UI to update.');
|
|
await waitFor(() => {
|
|
expect(mockedApiClient.uploadBrandLogo).toHaveBeenCalledWith(1, file);
|
|
expect(mockedToast.loading).toHaveBeenCalledWith('Uploading logo...');
|
|
expect(mockedToast.success).toHaveBeenCalledWith('Logo updated successfully!', {
|
|
id: 'toast-1',
|
|
});
|
|
// Check if the UI updates with the new logo
|
|
expect(screen.getByAltText('No Frills logo')).toHaveAttribute(
|
|
'src',
|
|
'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 () => {
|
|
console.log('TEST START: should handle failed logo upload with a non-Error object');
|
|
mockedApiClient.fetchAllBrands.mockImplementation(
|
|
async () => new Response(JSON.stringify(mockBrands), { status: 200 }),
|
|
);
|
|
// Reject with a string instead of an Error object to test the fallback error handling
|
|
mockedApiClient.uploadBrandLogo.mockRejectedValue('A string error');
|
|
mockedToast.loading.mockReturnValue('toast-non-error');
|
|
|
|
renderWithProviders(<AdminBrandManager />);
|
|
await waitFor(() => expect(screen.getByText('No Frills')).toBeInTheDocument());
|
|
|
|
const file = new File(['logo'], 'logo.png', { type: 'image/png' });
|
|
const input = screen.getByLabelText('Upload logo for No Frills');
|
|
|
|
fireEvent.change(input, { target: { files: [file] } });
|
|
|
|
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', {
|
|
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 () => {
|
|
console.log('TEST START: should handle failed logo upload');
|
|
console.log('TEST SETUP: Mocking fetchAllBrands for success and uploadBrandLogo for failure.');
|
|
mockedApiClient.fetchAllBrands.mockImplementation(
|
|
async () => new Response(JSON.stringify(mockBrands), { status: 200 }),
|
|
);
|
|
mockedApiClient.uploadBrandLogo.mockRejectedValue(new Error('Upload failed'));
|
|
mockedToast.loading.mockReturnValue('toast-2');
|
|
|
|
console.log('TEST ACTION: Rendering AdminBrandManager component.');
|
|
renderWithProviders(<AdminBrandManager />);
|
|
console.log('TEST ACTION: Waiting for initial brands to render.');
|
|
await waitFor(() => expect(screen.getByText('No Frills')).toBeInTheDocument());
|
|
|
|
const file = new File(['logo'], 'logo.png', { type: 'image/png' });
|
|
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] } });
|
|
|
|
console.log('TEST ASSERTION: Waiting for error toast to be called.');
|
|
await waitFor(() => {
|
|
expect(mockedToast.error).toHaveBeenCalledWith('Upload failed: Upload failed', {
|
|
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 () => {
|
|
console.log('TEST START: should show an error toast for invalid file type');
|
|
console.log('TEST SETUP: Mocking fetchAllBrands to resolve successfully.');
|
|
mockedApiClient.fetchAllBrands.mockImplementation(
|
|
async () => new Response(JSON.stringify(mockBrands), { status: 200 }),
|
|
);
|
|
console.log('TEST ACTION: Rendering AdminBrandManager component.');
|
|
renderWithProviders(<AdminBrandManager />);
|
|
console.log('TEST ACTION: Waiting for initial brands to render.');
|
|
await waitFor(() => expect(screen.getByText('No Frills')).toBeInTheDocument());
|
|
|
|
const file = new File(['text'], 'document.txt', { type: 'text/plain' });
|
|
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] } });
|
|
|
|
console.log('TEST ASSERTION: Waiting for validation error toast.');
|
|
await waitFor(() => {
|
|
expect(mockedToast.error).toHaveBeenCalledWith(
|
|
'Invalid file type. Please upload a PNG, JPG, WEBP, or SVG.',
|
|
);
|
|
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 () => {
|
|
console.log('TEST START: should show an error toast for oversized file');
|
|
console.log('TEST SETUP: Mocking fetchAllBrands to resolve successfully.');
|
|
mockedApiClient.fetchAllBrands.mockImplementation(
|
|
async () => new Response(JSON.stringify(mockBrands), { status: 200 }),
|
|
);
|
|
console.log('TEST ACTION: Rendering AdminBrandManager component.');
|
|
renderWithProviders(<AdminBrandManager />);
|
|
console.log('TEST ACTION: Waiting for initial brands to render.');
|
|
await waitFor(() => expect(screen.getByText('No Frills')).toBeInTheDocument());
|
|
|
|
const file = new File(['a'.repeat(3 * 1024 * 1024)], 'large.png', { type: 'image/png' });
|
|
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] } });
|
|
|
|
console.log('TEST ASSERTION: Waiting for size validation error toast.');
|
|
await waitFor(() => {
|
|
expect(mockedToast.error).toHaveBeenCalledWith('File is too large. Maximum size is 2MB.');
|
|
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 () => {
|
|
console.log('TEST START: should handle non-ok response from upload API');
|
|
mockedApiClient.fetchAllBrands.mockImplementation(
|
|
async () => new Response(JSON.stringify(mockBrands), { status: 200 }),
|
|
);
|
|
// Mock a failed response (e.g., 400 Bad Request)
|
|
mockedApiClient.uploadBrandLogo.mockResolvedValue(
|
|
new Response('Invalid image format', { status: 400 }),
|
|
);
|
|
mockedToast.loading.mockReturnValue('toast-3');
|
|
|
|
renderWithProviders(<AdminBrandManager />);
|
|
await waitFor(() => expect(screen.getByText('No Frills')).toBeInTheDocument());
|
|
|
|
const file = new File(['logo'], 'logo.png', { type: 'image/png' });
|
|
const input = screen.getByLabelText('Upload logo for No Frills');
|
|
|
|
fireEvent.change(input, { target: { files: [file] } });
|
|
|
|
await waitFor(() => {
|
|
expect(mockedToast.error).toHaveBeenCalledWith('Upload failed: Invalid image format', {
|
|
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 () => {
|
|
console.log('TEST START: should show an error toast if no file is selected');
|
|
console.log('TEST SETUP: Mocking fetchAllBrands to resolve successfully.');
|
|
mockedApiClient.fetchAllBrands.mockImplementation(
|
|
async () => new Response(JSON.stringify(mockBrands), { status: 200 }),
|
|
);
|
|
renderWithProviders(<AdminBrandManager />);
|
|
console.log('TEST ACTION: Waiting for initial brands to render.');
|
|
await waitFor(() => expect(screen.getByText('No Frills')).toBeInTheDocument());
|
|
|
|
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: [] } });
|
|
|
|
console.log('TEST ASSERTION: Waiting for the "no file selected" error toast.');
|
|
await waitFor(() => {
|
|
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 () => {
|
|
mockedApiClient.fetchAllBrands.mockImplementation(
|
|
async () => new Response(JSON.stringify([]), { status: 200 }),
|
|
);
|
|
renderWithProviders(<AdminBrandManager />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByRole('heading', { name: /brand management/i })).toBeInTheDocument();
|
|
// Only the header row should be present
|
|
expect(screen.getAllByRole('row')).toHaveLength(1);
|
|
});
|
|
});
|
|
|
|
it('should use status code in error message if response body is empty on upload failure', async () => {
|
|
mockedApiClient.fetchAllBrands.mockImplementation(
|
|
async () => new Response(JSON.stringify(mockBrands), { status: 200 }),
|
|
);
|
|
mockedApiClient.uploadBrandLogo.mockImplementation(
|
|
async () => new Response(null, { status: 500, statusText: 'Internal Server Error' }),
|
|
);
|
|
mockedToast.loading.mockReturnValue('toast-fallback');
|
|
|
|
renderWithProviders(<AdminBrandManager />);
|
|
await waitFor(() => expect(screen.getByText('No Frills')).toBeInTheDocument());
|
|
|
|
const file = new File(['logo'], 'logo.png', { type: 'image/png' });
|
|
const input = screen.getByLabelText('Upload logo for No Frills');
|
|
fireEvent.change(input, { target: { files: [file] } });
|
|
|
|
await waitFor(() => {
|
|
expect(mockedToast.error).toHaveBeenCalledWith(
|
|
'Upload failed: Upload failed with status 500',
|
|
{ id: 'toast-fallback' },
|
|
);
|
|
});
|
|
});
|
|
|
|
it('should only update the target brand logo and leave others unchanged', async () => {
|
|
mockedApiClient.fetchAllBrands.mockImplementation(
|
|
async () => new Response(JSON.stringify(mockBrands), { status: 200 }),
|
|
);
|
|
mockedApiClient.uploadBrandLogo.mockImplementation(
|
|
async () => new Response(JSON.stringify({ logoUrl: 'new-logo.png' }), { status: 200 }),
|
|
);
|
|
mockedToast.loading.mockReturnValue('toast-opt');
|
|
|
|
renderWithProviders(<AdminBrandManager />);
|
|
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 input = screen.getByLabelText('Upload logo for No Frills'); // Brand 1
|
|
fireEvent.change(input, { target: { files: [file] } });
|
|
|
|
await waitFor(() => {
|
|
// Brand 1 should have new logo
|
|
expect(screen.getByAltText('No Frills logo')).toHaveAttribute('src', 'new-logo.png');
|
|
// Brand 2 should still have original logo
|
|
expect(screen.getByAltText('Compliments logo')).toHaveAttribute(
|
|
'src',
|
|
'https://example.com/compliments.png',
|
|
);
|
|
});
|
|
});
|
|
});
|