diff --git a/src/pages/sidepanel/sections/ProfileSummary.test.tsx b/src/pages/sidepanel/sections/ProfileSummary.test.tsx new file mode 100644 index 0000000..2a5a8df --- /dev/null +++ b/src/pages/sidepanel/sections/ProfileSummary.test.tsx @@ -0,0 +1,149 @@ +import React from 'react'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import ProfileSummary from './ProfileSummary'; +import { ThemeProvider } from '@mui/material/styles'; +import { appTheme } from '@root/src/theme'; + +vi.mock('@root/src/pages/sidepanel/sections/profile/ProfileHeader', () => ({ + default: ({ lyticsId }) =>
{lyticsId}
, +})); + +vi.mock('@root/src/pages/sidepanel/sections/profile/AudienceMembership', () => ({ + default: ({ audiences }) =>
{audiences.join(', ')}
, +})); + +vi.mock('@root/src/pages/sidepanel/sections/profile/Attributes', () => ({ + default: ({ count }) =>
Count: {count}
, +})); + +vi.mock('@root/src/pages/sidepanel/sections/profile/BehaviorMetrics', () => ({ + default: ({ metrics }) =>
{metrics.length} metrics
, +})); + +vi.mock('@root/src/pages/sidepanel/sections/profile/Interests', () => ({ + default: ({ interests }) => ( +
+ {interests.map((interest, index) => ( +
+ {interest.label}: {interest.value}% +
+ ))} +
+ ), +})); + +vi.mock('@root/src/pages/sidepanel/sections/profile/ProfileMetadata', () => ({ + default: () =>
Metadata
, +})); + +describe('ProfileSummary', () => { + const renderWithTheme = component => { + return render({component}); + }; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('transforms lytics_content object to interests array correctly', () => { + const mockProfile = { + data: { + user: { + _id: 'test-id', + _uid: 'test-uid', + lytics_content: { + Home: 1, + Soap: 0.8409454388537114, + }, + }, + }, + }; + + renderWithTheme(); + + const interestsContainer = screen.getByTestId('interests'); + expect(interestsContainer).toBeInTheDocument(); + + expect(screen.getByTestId('interest-0')).toHaveTextContent('Home: 100%'); + expect(screen.getByTestId('interest-1')).toHaveTextContent('Soap: 84%'); + }); + + it('handles missing lytics_content gracefully', () => { + const mockProfile = { + data: { + user: { + _id: 'test-id', + _uid: 'test-uid', + }, + }, + }; + + renderWithTheme(); + + const interestsContainer = screen.getByTestId('interests'); + expect(interestsContainer).toBeInTheDocument(); + expect(interestsContainer.children).toHaveLength(0); + }); + + it('converts interest values to percentages correctly', () => { + const mockProfile = { + data: { + user: { + _id: 'test-id', + _uid: 'test-uid', + lytics_content: { + Technology: 0.95, + Sports: 0.6, + Music: 0.45, + Full: 1.0, + Zero: 0, + }, + }, + }, + }; + + renderWithTheme(); + + expect(screen.getByTestId('interest-0')).toHaveTextContent('Technology: 95%'); + expect(screen.getByTestId('interest-1')).toHaveTextContent('Sports: 60%'); + expect(screen.getByTestId('interest-2')).toHaveTextContent('Music: 45%'); + expect(screen.getByTestId('interest-3')).toHaveTextContent('Full: 100%'); + expect(screen.getByTestId('interest-4')).toHaveTextContent('Zero: 0%'); + }); + + it('handles null or undefined lytics_content', () => { + const mockProfile = { + data: { + user: { + _id: 'test-id', + _uid: 'test-uid', + lytics_content: null, + }, + }, + }; + + renderWithTheme(); + + const interestsContainer = screen.getByTestId('interests'); + expect(interestsContainer.children).toHaveLength(0); + }); + + it('handles empty lytics_content object', () => { + const mockProfile = { + data: { + user: { + _id: 'test-id', + _uid: 'test-uid', + lytics_content: {}, + }, + }, + }; + + renderWithTheme(); + + const interestsContainer = screen.getByTestId('interests'); + expect(interestsContainer.children).toHaveLength(0); + }); +}); diff --git a/src/pages/sidepanel/sections/ProfileSummary.tsx b/src/pages/sidepanel/sections/ProfileSummary.tsx index 04c220f..76a88f6 100644 --- a/src/pages/sidepanel/sections/ProfileSummary.tsx +++ b/src/pages/sidepanel/sections/ProfileSummary.tsx @@ -92,6 +92,16 @@ const ProfileSummary: React.FC = ({ profile }) => { return attributes.length > 0 ? attributes[0] : 'Unknown'; }, [profile?.data?.user]); + const interestsArray = useMemo(() => { + const lyticsContent = profile?.data?.user?.lytics_content; + if (!lyticsContent || typeof lyticsContent !== 'object') return []; + + return Object.entries(lyticsContent).map(([label, value]) => ({ + label, + value: Math.round((typeof value === 'number' ? value : 0) * 100), + })); + }, [profile?.data?.user?.lytics_content]); + return ( = ({ profile }) => { /> )} - + diff --git a/src/pages/sidepanel/sections/profile/Interests.test.tsx b/src/pages/sidepanel/sections/profile/Interests.test.tsx new file mode 100644 index 0000000..d40aaf5 --- /dev/null +++ b/src/pages/sidepanel/sections/profile/Interests.test.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import { describe, it, expect } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { Interests } from './Interests'; +import { ThemeProvider } from '@mui/material/styles'; +import { appTheme } from '@root/src/theme'; + +describe('Interests', () => { + const renderWithTheme = component => { + return render({component}); + }; + + it('renders interests with progress bars when data is available', () => { + const mockInterests = [ + { label: 'Home', value: 100 }, + { label: 'Soap', value: 84 }, + ]; + + renderWithTheme(); + + expect(screen.getByText('Interests')).toBeInTheDocument(); + expect(screen.getByText('Home')).toBeInTheDocument(); + expect(screen.getByText('Soap')).toBeInTheDocument(); + + const progressBars = screen.getAllByRole('progressbar'); + expect(progressBars).toHaveLength(2); + expect(progressBars[0]).toHaveAttribute('aria-label', 'Home: 100%'); + expect(progressBars[1]).toHaveAttribute('aria-label', 'Soap: 84%'); + }); + + it('displays empty state when hasData is false', () => { + renderWithTheme(); + + expect(screen.getByText('Interests')).toBeInTheDocument(); + expect(screen.getByText(/Interests are not currently shared for this account/)).toBeInTheDocument(); + expect(screen.getByText('Learn more')).toBeInTheDocument(); + }); + + it('displays empty state when interests array is empty', () => { + renderWithTheme(); + + expect(screen.getByText('Interests')).toBeInTheDocument(); + expect(screen.getByText(/Interests are not currently shared for this account/)).toBeInTheDocument(); + }); + + it('renders multiple interests correctly', () => { + const mockInterests = [ + { label: 'Technology', value: 95 }, + { label: 'Sports', value: 60 }, + { label: 'Music', value: 45 }, + ]; + + renderWithTheme(); + + expect(screen.getByText('Technology')).toBeInTheDocument(); + expect(screen.getByText('Sports')).toBeInTheDocument(); + expect(screen.getByText('Music')).toBeInTheDocument(); + + const progressBars = screen.getAllByRole('progressbar'); + expect(progressBars).toHaveLength(3); + }); +}); diff --git a/src/pages/sidepanel/sections/profile/Interests.tsx b/src/pages/sidepanel/sections/profile/Interests.tsx index b120c73..fe61a09 100644 --- a/src/pages/sidepanel/sections/profile/Interests.tsx +++ b/src/pages/sidepanel/sections/profile/Interests.tsx @@ -1,13 +1,18 @@ import React from 'react'; -import { Box, Stack, Typography, Link } from '@mui/material'; +import { Box, Stack, Typography, Link, LinearProgress } from '@mui/material'; import { styled } from '@mui/material/styles'; import { Lock } from '@mui/icons-material'; import { appColors } from '@root/src/theme/palette'; import { appContent } from '@root/src/shared/content/appContent'; +interface Interest { + label: string; + value: number; +} + interface InterestsProps { hasData: boolean; - interests?: string[]; + interests?: Interest[]; textContent?: typeof appContent.interests; } @@ -68,11 +73,33 @@ const StyledLink = styled(Link)(() => ({ }, })); -const ContentText = styled(Typography)(() => ({ +const InterestsContainer = styled(Stack)(({ theme }) => ({ + display: 'flex', + flexDirection: 'column', + gap: theme.spacing(1), +})); + +const InterestRow = styled(Stack)(({ theme }) => ({ + display: 'flex', + flexDirection: 'column', + gap: theme.spacing(0.5), +})); + +const InterestLabel = styled(Typography)(() => ({ fontSize: appColors.common.fontSize.baseSmall, - color: appColors.neutral[600], - lineHeight: appColors.common.lineHeight.tight, fontWeight: appColors.common.fontWeight.medium, + color: appColors.neutral[900], +})); + +const StyledLinearProgress = styled(LinearProgress)(({ theme }) => ({ + width: '100%', + height: '0.5rem', // 8px + borderRadius: theme.spacing(0.5), + backgroundColor: appColors.common.colors.accentLight, + '& .MuiLinearProgress-bar': { + backgroundColor: appColors.common.colors.accent, + borderRadius: theme.spacing(0.5), + }, })); export const Interests = ({ @@ -85,7 +112,18 @@ export const Interests = ({ {textContent.title} {hasData && interests.length > 0 ? ( - {interests.join(', ')} + + {interests.map((interest, index) => ( + + {interest.label} + + + ))} + ) : (