Files
flyer-crawler.projectium.com/src/pages/admin/ActivityLog.test.tsx
Torben Sorensen 503e7084da
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 14m41s
Adopt TanStack Query fixes
2026-01-10 17:42:45 -08:00

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();
});
});