Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 149 additions & 0 deletions src/pages/sidepanel/sections/ProfileSummary.test.tsx
Original file line number Diff line number Diff line change
@@ -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 }) => <div data-testid="profile-header">{lyticsId}</div>,
}));

vi.mock('@root/src/pages/sidepanel/sections/profile/AudienceMembership', () => ({
default: ({ audiences }) => <div data-testid="audience-membership">{audiences.join(', ')}</div>,
}));

vi.mock('@root/src/pages/sidepanel/sections/profile/Attributes', () => ({
default: ({ count }) => <div data-testid="attributes">Count: {count}</div>,
}));

vi.mock('@root/src/pages/sidepanel/sections/profile/BehaviorMetrics', () => ({
default: ({ metrics }) => <div data-testid="behavior-metrics">{metrics.length} metrics</div>,
}));

vi.mock('@root/src/pages/sidepanel/sections/profile/Interests', () => ({
default: ({ interests }) => (
<div data-testid="interests">
{interests.map((interest, index) => (
<div key={index} data-testid={`interest-${index}`}>
{interest.label}: {interest.value}%
</div>
))}
</div>
),
}));

vi.mock('@root/src/pages/sidepanel/sections/profile/ProfileMetadata', () => ({
default: () => <div data-testid="profile-metadata">Metadata</div>,
}));

describe('ProfileSummary', () => {
const renderWithTheme = component => {
return render(<ThemeProvider theme={appTheme}>{component}</ThemeProvider>);
};

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(<ProfileSummary profile={mockProfile} />);

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(<ProfileSummary profile={mockProfile} />);

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(<ProfileSummary profile={mockProfile} />);

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(<ProfileSummary profile={mockProfile} />);

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(<ProfileSummary profile={mockProfile} />);

const interestsContainer = screen.getByTestId('interests');
expect(interestsContainer.children).toHaveLength(0);
});
});
12 changes: 11 additions & 1 deletion src/pages/sidepanel/sections/ProfileSummary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,16 @@ const ProfileSummary: React.FC<ProfileSummaryTabProps> = ({ 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 (
<Box
sx={{
Expand Down Expand Up @@ -126,7 +136,7 @@ const ProfileSummary: React.FC<ProfileSummaryTabProps> = ({ profile }) => {
/>
)}

<Interests hasData={hasContent} interests={profile?.data?.user?.lytics_content || []} />
<Interests hasData={hasContent} interests={interestsArray} />

<ProfileMetadata lastUpdated={lastUpdatedText} lastAttribute={lastAttributeText} />
</Box>
Expand Down
63 changes: 63 additions & 0 deletions src/pages/sidepanel/sections/profile/Interests.test.tsx
Original file line number Diff line number Diff line change
@@ -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(<ThemeProvider theme={appTheme}>{component}</ThemeProvider>);
};

it('renders interests with progress bars when data is available', () => {
const mockInterests = [
{ label: 'Home', value: 100 },
{ label: 'Soap', value: 84 },
];

renderWithTheme(<Interests hasData={true} interests={mockInterests} />);

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(<Interests hasData={false} interests={[]} />);

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(<Interests hasData={true} interests={[]} />);

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(<Interests hasData={true} interests={mockInterests} />);

expect(screen.getByText('Technology')).toBeInTheDocument();
expect(screen.getByText('Sports')).toBeInTheDocument();
expect(screen.getByText('Music')).toBeInTheDocument();

const progressBars = screen.getAllByRole('progressbar');
expect(progressBars).toHaveLength(3);
});
});
50 changes: 44 additions & 6 deletions src/pages/sidepanel/sections/profile/Interests.tsx
Original file line number Diff line number Diff line change
@@ -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;
}

Expand Down Expand Up @@ -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 = ({
Expand All @@ -85,7 +112,18 @@ export const Interests = ({
<TitleText>{textContent.title}</TitleText>

{hasData && interests.length > 0 ? (
<ContentText>{interests.join(', ')}</ContentText>
<InterestsContainer>
{interests.map((interest, index) => (
<InterestRow key={index}>
<InterestLabel>{interest.label}</InterestLabel>
<StyledLinearProgress
variant="determinate"
value={interest.value}
aria-label={`${interest.label}: ${interest.value}%`}
/>
</InterestRow>
))}
</InterestsContainer>
) : (
<EmptyStateContainer>
<LockIcon aria-hidden="true" />
Expand Down
Loading