All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 14m41s
278 lines
9.4 KiB
TypeScript
278 lines
9.4 KiB
TypeScript
// src/pages/admin/ActivityLog.test.tsx
|
|
import React from 'react';
|
|
import { render, screen, fireEvent } from '@testing-library/react';
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
import { ActivityLog } from './ActivityLog';
|
|
import { useActivityLogQuery } from '../../hooks/queries/useActivityLogQuery';
|
|
import type { ActivityLogItem, UserProfile } from '../../types';
|
|
import { createMockActivityLogItem, createMockUserProfile } from '../../tests/utils/mockFactories';
|
|
import { QueryWrapper } from '../../tests/utils/renderWithProviders';
|
|
|
|
// Mock the TanStack Query hook
|
|
vi.mock('../../hooks/queries/useActivityLogQuery');
|
|
|
|
const renderWithQuery = (ui: React.ReactElement) => render(ui, { wrapper: QueryWrapper });
|
|
|
|
const mockedUseActivityLogQuery = vi.mocked(useActivityLogQuery);
|
|
|
|
// Mock date-fns to return a consistent value for snapshots
|
|
vi.mock('date-fns', () => {
|
|
return {
|
|
formatDistanceToNow: vi.fn(() => 'about 5 hours ago'),
|
|
};
|
|
});
|
|
|
|
const mockUserProfile: UserProfile = createMockUserProfile({
|
|
user: { user_id: 'user-123', email: 'test@example.com' },
|
|
});
|
|
|
|
const mockLogs: ActivityLogItem[] = [
|
|
createMockActivityLogItem({
|
|
activity_log_id: 1,
|
|
user_id: 'user-123',
|
|
action: 'flyer_processed',
|
|
display_text: 'Processed a new flyer for Walmart.',
|
|
user_avatar_url: 'https://example.com/avatar.png',
|
|
user_full_name: 'Test User',
|
|
details: { flyer_id: 1, store_name: 'Walmart' },
|
|
}),
|
|
createMockActivityLogItem({
|
|
activity_log_id: 2,
|
|
user_id: 'user-456',
|
|
action: 'recipe_created',
|
|
display_text: 'Jane Doe added a new recipe: Pasta Carbonara',
|
|
user_full_name: 'Jane Doe',
|
|
details: { recipe_id: 1, recipe_name: 'Pasta Carbonara' },
|
|
}),
|
|
createMockActivityLogItem({
|
|
activity_log_id: 3,
|
|
user_id: 'user-789',
|
|
action: 'list_shared',
|
|
display_text: 'John Smith shared a list.',
|
|
user_full_name: 'John Smith',
|
|
details: { list_name: 'Weekly Groceries', shopping_list_id: 10, shared_with_name: 'Test User' },
|
|
}),
|
|
createMockActivityLogItem({
|
|
activity_log_id: 4,
|
|
user_id: 'user-101',
|
|
action: 'user_registered',
|
|
display_text: 'New user joined',
|
|
details: { full_name: 'Newbie User' },
|
|
}),
|
|
createMockActivityLogItem({
|
|
activity_log_id: 5,
|
|
user_id: 'user-102',
|
|
action: 'recipe_favorited',
|
|
display_text: 'User favorited a recipe',
|
|
user_full_name: 'Pizza Lover',
|
|
user_avatar_url: 'https://example.com/pizza.png',
|
|
details: { recipe_name: 'Best Pizza' },
|
|
}),
|
|
createMockActivityLogItem({
|
|
activity_log_id: 6,
|
|
user_id: 'user-103',
|
|
action: 'unknown_action' as any,
|
|
display_text: 'Something happened',
|
|
details: {} as any,
|
|
}),
|
|
];
|
|
|
|
describe('ActivityLog', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
// Default mock implementation
|
|
mockedUseActivityLogQuery.mockReturnValue({
|
|
data: [],
|
|
isLoading: false,
|
|
error: null,
|
|
} as any);
|
|
});
|
|
|
|
it('should not render if userProfile is null', () => {
|
|
const { container } = renderWithQuery(<ActivityLog userProfile={null} onLogClick={vi.fn()} />);
|
|
expect(container).toBeEmptyDOMElement();
|
|
});
|
|
|
|
it('should show a loading state initially', async () => {
|
|
mockedUseActivityLogQuery.mockReturnValue({
|
|
data: undefined,
|
|
isLoading: true,
|
|
error: null,
|
|
} as any);
|
|
|
|
renderWithQuery(<ActivityLog userProfile={mockUserProfile} onLogClick={vi.fn()} />);
|
|
|
|
expect(screen.getByText('Loading activity...')).toBeInTheDocument();
|
|
});
|
|
|
|
it('should display an error message if fetching logs fails', async () => {
|
|
mockedUseActivityLogQuery.mockReturnValue({
|
|
data: undefined,
|
|
isLoading: false,
|
|
error: new Error('API is down'),
|
|
} as any);
|
|
|
|
renderWithQuery(<ActivityLog userProfile={mockUserProfile} onLogClick={vi.fn()} />);
|
|
expect(screen.getByText('API is down')).toBeInTheDocument();
|
|
});
|
|
|
|
it('should display a message when there are no logs', async () => {
|
|
mockedUseActivityLogQuery.mockReturnValue({
|
|
data: [],
|
|
isLoading: false,
|
|
error: null,
|
|
} as any);
|
|
|
|
renderWithQuery(<ActivityLog userProfile={mockUserProfile} onLogClick={vi.fn()} />);
|
|
expect(screen.getByText('No recent activity to show.')).toBeInTheDocument();
|
|
});
|
|
|
|
it('should render a list of activities successfully covering all types', async () => {
|
|
mockedUseActivityLogQuery.mockReturnValue({
|
|
data: mockLogs,
|
|
isLoading: false,
|
|
error: null,
|
|
} as any);
|
|
|
|
renderWithQuery(<ActivityLog userProfile={mockUserProfile} />);
|
|
|
|
// Check for specific text from different log types
|
|
expect(screen.getByText('Walmart')).toBeInTheDocument();
|
|
expect(screen.getByText('Pasta Carbonara')).toBeInTheDocument();
|
|
expect(screen.getByText('Weekly Groceries')).toBeInTheDocument();
|
|
expect(screen.getByText('Newbie User')).toBeInTheDocument();
|
|
expect(screen.getByText('Best Pizza')).toBeInTheDocument();
|
|
expect(screen.getByText('An unknown activity occurred.')).toBeInTheDocument();
|
|
|
|
// Check for user names
|
|
expect(screen.getByText('Jane Doe', { exact: false })).toBeInTheDocument();
|
|
|
|
// Check for avatar
|
|
const avatar = screen.getByAltText('Test User');
|
|
expect(avatar).toBeInTheDocument();
|
|
expect(avatar).toHaveAttribute('src', 'https://example.com/avatar.png');
|
|
|
|
// Check for fallback avatar (Newbie User has no avatar)
|
|
const newbieItem = screen.getByText('Newbie User').closest('li');
|
|
const fallbackIcon = newbieItem?.querySelector('svg');
|
|
expect(fallbackIcon).toBeInTheDocument();
|
|
|
|
// Check for the mocked date
|
|
expect(screen.getAllByText('about 5 hours ago')).toHaveLength(mockLogs.length);
|
|
});
|
|
|
|
it('should call onLogClick when a clickable log item is clicked', async () => {
|
|
const onLogClickMock = vi.fn();
|
|
mockedUseActivityLogQuery.mockReturnValue({
|
|
data: mockLogs,
|
|
isLoading: false,
|
|
error: null,
|
|
} as any);
|
|
|
|
renderWithQuery(<ActivityLog userProfile={mockUserProfile} onLogClick={onLogClickMock} />);
|
|
|
|
// Recipe Created
|
|
const clickableRecipe = screen.getByText('Pasta Carbonara');
|
|
fireEvent.click(clickableRecipe);
|
|
expect(onLogClickMock).toHaveBeenCalledWith(mockLogs[1]);
|
|
|
|
// List Shared
|
|
const clickableList = screen.getByText('Weekly Groceries');
|
|
fireEvent.click(clickableList);
|
|
expect(onLogClickMock).toHaveBeenCalledWith(mockLogs[2]);
|
|
|
|
// Recipe Favorited
|
|
const clickableFav = screen.getByText('Best Pizza');
|
|
fireEvent.click(clickableFav);
|
|
expect(onLogClickMock).toHaveBeenCalledWith(mockLogs[4]);
|
|
|
|
expect(onLogClickMock).toHaveBeenCalledTimes(3);
|
|
});
|
|
|
|
it('should not render clickable styling if onLogClick is undefined', async () => {
|
|
mockedUseActivityLogQuery.mockReturnValue({
|
|
data: mockLogs,
|
|
isLoading: false,
|
|
error: null,
|
|
} as any);
|
|
|
|
renderWithQuery(<ActivityLog userProfile={mockUserProfile} />);
|
|
|
|
const recipeName = screen.getByText('Pasta Carbonara');
|
|
expect(recipeName).not.toHaveClass('cursor-pointer');
|
|
expect(recipeName).not.toHaveClass('text-blue-500');
|
|
|
|
const listName = screen.getByText('Weekly Groceries');
|
|
expect(listName).not.toHaveClass('cursor-pointer');
|
|
});
|
|
|
|
it('should handle missing details in logs gracefully (fallback values)', async () => {
|
|
const logsWithMissingDetails: ActivityLogItem[] = [
|
|
createMockActivityLogItem({
|
|
activity_log_id: 101,
|
|
user_id: 'u1',
|
|
action: 'flyer_processed',
|
|
display_text: '...',
|
|
details: { flyer_id: 1, store_name: '' } as any,
|
|
}),
|
|
createMockActivityLogItem({
|
|
activity_log_id: 102,
|
|
user_id: 'u2',
|
|
action: 'recipe_created',
|
|
display_text: '...',
|
|
details: { recipe_id: 1, recipe_name: '' } as any,
|
|
}),
|
|
createMockActivityLogItem({
|
|
activity_log_id: 103,
|
|
user_id: 'u3',
|
|
action: 'user_registered',
|
|
display_text: '...',
|
|
details: { full_name: '' } as any,
|
|
}),
|
|
createMockActivityLogItem({
|
|
activity_log_id: 104,
|
|
user_id: 'u4',
|
|
action: 'recipe_favorited',
|
|
display_text: '...',
|
|
details: { recipe_id: 2, recipe_name: '' } as any,
|
|
}),
|
|
createMockActivityLogItem({
|
|
activity_log_id: 105,
|
|
user_id: 'u5',
|
|
action: 'list_shared',
|
|
display_text: '...',
|
|
details: { shopping_list_id: 1, list_name: '', shared_with_name: '' } as any,
|
|
}),
|
|
createMockActivityLogItem({
|
|
activity_log_id: 106,
|
|
user_id: 'u6',
|
|
action: 'flyer_processed',
|
|
display_text: '...',
|
|
user_avatar_url: 'http://img.com/a.png',
|
|
user_full_name: '',
|
|
details: { flyer_id: 2, store_name: 'Mock Store' } as any,
|
|
}),
|
|
];
|
|
|
|
mockedUseActivityLogQuery.mockReturnValue({
|
|
data: logsWithMissingDetails,
|
|
isLoading: false,
|
|
error: null,
|
|
} as any);
|
|
|
|
renderWithQuery(<ActivityLog userProfile={mockUserProfile} />);
|
|
|
|
expect(screen.getAllByText('a store')[0]).toBeInTheDocument();
|
|
expect(screen.getByText('Untitled Recipe')).toBeInTheDocument();
|
|
expect(screen.getByText('A new user')).toBeInTheDocument();
|
|
expect(screen.getByText('a recipe')).toBeInTheDocument();
|
|
expect(screen.getByText('a shopping list')).toBeInTheDocument();
|
|
expect(screen.getByText('another user')).toBeInTheDocument();
|
|
|
|
// Check for avatar with fallback alt text
|
|
const avatars = screen.getAllByRole('img');
|
|
const avatarWithFallbackAlt = avatars.find((img) => img.getAttribute('alt') === 'User Avatar');
|
|
expect(avatarWithFallbackAlt).toBeInTheDocument();
|
|
});
|
|
});
|