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
60 changes: 34 additions & 26 deletions components/__tests__/all-time-perlearner-box.test.tsx
Original file line number Diff line number Diff line change
@@ -1,44 +1,53 @@
import { describe, it, expect } from 'vitest';
import { describe, it, expect, vi } from 'vitest';
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';

vi.mock('next/navigation', () => ({
useParams: () => ({ tenant: 'test-tenant' }),
}));

vi.mock('@/utils/helpers', () => ({
getTenant: vi.fn(() => 'test-tenant'),
}));

import { AllTimePerLearnerBox } from '../all-time-perlearner-box';

describe('AllTimePerLearnerBox', () => {
const defaultProps = {
total_assessments: 10,
total_time_spent: 7200,
total_videos: 5,
course_completions: 3,
courses: 4,
credentials: 2,
skills: 6,
};

it('renders without crashing', () => {
const { container } = render(<AllTimePerLearnerBox {...defaultProps} />);
expect(container.firstChild).toBeInTheDocument();
});

it('renders "All Time" heading', () => {
it('renders "Highlights" heading', () => {
render(<AllTimePerLearnerBox {...defaultProps} />);
expect(screen.getByText('All Time')).toBeInTheDocument();
expect(screen.getByText('Highlights')).toBeInTheDocument();
});

it('renders Time Spent label', () => {
render(<AllTimePerLearnerBox {...defaultProps} />);
expect(screen.getByText('Time Spent')).toBeInTheDocument();
});

it('renders Watched Video label', () => {
it('renders Courses label', () => {
render(<AllTimePerLearnerBox {...defaultProps} />);
expect(screen.getByText('Watched Video')).toBeInTheDocument();
expect(screen.getByText('Courses')).toBeInTheDocument();
});

it('renders Assessments label', () => {
it('renders Credentials label', () => {
render(<AllTimePerLearnerBox {...defaultProps} />);
expect(screen.getByText('Assessments')).toBeInTheDocument();
expect(screen.getByText('Credentials')).toBeInTheDocument();
});

it('renders Courses Completions label', () => {
it('renders Skills label', () => {
render(<AllTimePerLearnerBox {...defaultProps} />);
expect(screen.getByText('Courses Completions')).toBeInTheDocument();
expect(screen.getByText('Skills')).toBeInTheDocument();
});

it('converts time spent to hours correctly', () => {
Expand All @@ -47,19 +56,25 @@ describe('AllTimePerLearnerBox', () => {
expect(screen.getByText('2 hours')).toBeInTheDocument();
});

it('displays total videos count', () => {
it('displays courses count as a link to the profile courses page', () => {
render(<AllTimePerLearnerBox {...defaultProps} />);
expect(screen.getByText('5')).toBeInTheDocument();
const link = screen.getByRole('link', { name: 'View courses' });
expect(link).toHaveTextContent('4');
expect(link).toHaveAttribute('href', '/platform/test-tenant/profile/courses');
});

it('displays total assessments count', () => {
it('displays credentials count as a link to the profile credentials page', () => {
render(<AllTimePerLearnerBox {...defaultProps} />);
expect(screen.getByText('10')).toBeInTheDocument();
const link = screen.getByRole('link', { name: 'View credentials' });
expect(link).toHaveTextContent('2');
expect(link).toHaveAttribute('href', '/platform/test-tenant/profile/credentials');
});

it('displays course completions count', () => {
it('displays skills count as a link to the profile skills page', () => {
render(<AllTimePerLearnerBox {...defaultProps} />);
expect(screen.getByText('3')).toBeInTheDocument();
const link = screen.getByRole('link', { name: 'View skills' });
expect(link).toHaveTextContent('6');
expect(link).toHaveAttribute('href', '/platform/test-tenant/profile/skills');
});

it('handles zero time spent', () => {
Expand All @@ -84,14 +99,7 @@ describe('AllTimePerLearnerBox', () => {
});

it('handles zero values for all props', () => {
render(
<AllTimePerLearnerBox
total_assessments={0}
total_time_spent={0}
total_videos={0}
course_completions={0}
/>,
);
render(<AllTimePerLearnerBox total_time_spent={0} courses={0} credentials={0} skills={0} />);
expect(screen.getByText('0 hours')).toBeInTheDocument();
// All zeros displayed
const zeros = screen.getAllByText('0');
Expand Down
6 changes: 2 additions & 4 deletions components/__tests__/credentials-list-box.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,9 @@ describe('CredentialsListBox', () => {
expect(screen.getByText('Credential B')).toBeInTheDocument();
});

it('shows empty message when no credentials', () => {
it('does not show an empty message when no credentials', () => {
render(<CredentialsListBox credentials={[]} />);
// DefaultEmptyBox is rendered with message "No credentials yet."
// Since we're not mocking DefaultEmptyBox, it renders the actual component
expect(screen.getByText('No credentials yet.')).toBeInTheDocument();
expect(screen.queryByText('No credentials yet.')).not.toBeInTheDocument();
});

it('renders correct number of credential items', () => {
Expand Down
25 changes: 21 additions & 4 deletions components/__tests__/profile-sidebar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ vi.mock('../credentials-list-box', () => ({

vi.mock('../all-time-perlearner-box', () => ({
AllTimePerLearnerBox: (props: any) => (
<div data-testid="all-time-box">All Time: {props.total_time_spent}</div>
<div data-testid="all-time-box">
All Time: {props.total_time_spent} / {props.courses} / {props.credentials} / {props.skills}
</div>
),
}));

Expand Down Expand Up @@ -56,6 +58,12 @@ const mockUseUserMetadata = vi.fn(() => ({
userMetaDataLoading: false,
}));

const mockUseAllTimeStats = vi.fn(() => ({
courses: 4,
credentials: 2,
skills: 6,
}));

vi.mock('@/hooks/skills/use-latest-skills', () => ({
useLatestSkills: () => mockUseLatestSkills(),
}));
Expand All @@ -72,6 +80,10 @@ vi.mock('@/hooks/users/use-usermetadata', () => ({
useUserMetadata: () => mockUseUserMetadata(),
}));

vi.mock('@/hooks/profile/use-all-time-stats', () => ({
useAllTimeStats: () => mockUseAllTimeStats(),
}));

import { ProfileSidebar } from '../profile-sidebar';

describe('ProfileSidebar', () => {
Expand All @@ -95,6 +107,11 @@ describe('ProfileSidebar', () => {
},
userPerLearnerInfoLoading: false,
});
mockUseAllTimeStats.mockReturnValue({
courses: 4,
credentials: 2,
skills: 6,
});
});

it('renders without crashing', () => {
Expand Down Expand Up @@ -143,15 +160,15 @@ describe('ProfileSidebar', () => {

it('passes correct props to AllTimePerLearnerBox', () => {
render(<ProfileSidebar />);
expect(screen.getByText('All Time: 3600')).toBeInTheDocument();
expect(screen.getByText('All Time: 3600 / 4 / 2 / 6')).toBeInTheDocument();
});

it('defaults to 0 when perlearner info values are undefined', () => {
it('defaults time spent to 0 when perlearner info values are undefined', () => {
mockUsePerLearnerInfoQuery.mockReturnValue({
userPerLearnerInfo: undefined,
userPerLearnerInfoLoading: false,
} as any);
render(<ProfileSidebar />);
expect(screen.getByText('All Time: 0')).toBeInTheDocument();
expect(screen.getByText('All Time: 0 / 4 / 2 / 6')).toBeInTheDocument();
});
});
58 changes: 41 additions & 17 deletions components/all-time-perlearner-box.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,32 @@
import { Award, Clock, FileText, Play } from 'lucide-react';
'use client';
import { Award, BookOpen, Clock, Sparkles } from 'lucide-react';
import Link from 'next/link';
import { useTenantParam } from '@/hooks/use-tenant-param';

interface AllTimePerLearnerBoxProps {
total_assessments: number;
total_time_spent: number;
total_videos: number;
course_completions: number;
courses: number;
credentials: number;
skills: number;
}

export const AllTimePerLearnerBox = ({
total_assessments,
total_time_spent,
total_videos,
course_completions,
courses,
credentials,
skills,
}: AllTimePerLearnerBoxProps) => {
const tenant = useTenantParam();
const hours =
typeof total_time_spent === 'number' && !isNaN(total_time_spent)
? Math.round(total_time_spent / 3600)
: 0;

return (
<div className="mb-4 rounded-md border border-[var(--sidebar-border)] p-4">
<h3 className="mb-3 text-sm font-medium text-[var(--sidebar-text)] md:text-base">All Time</h3>
<h3 className="mb-3 text-sm font-medium text-[var(--sidebar-text)] md:text-base">
Highlights
</h3>
<div className="space-y-5">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
Expand All @@ -34,29 +40,47 @@ export const AllTimePerLearnerBox = ({
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<div className="flex h-5 w-5 items-center justify-center rounded-sm bg-[var(--sidebar-icon-bg)]">
<Play className="h-3 w-3 text-[var(--sidebar-icon-color)]" />
<BookOpen className="h-3 w-3 text-[var(--sidebar-icon-color)]" />
</div>
<span className="text-xs text-[var(--sidebar-text)]">Watched Video</span>
<span className="text-xs text-[var(--sidebar-text)]">Courses</span>
</div>
<span className="text-xs font-medium text-[var(--text-dark)]">{total_videos}</span>
<Link
href={`/platform/${tenant}/profile/courses`}
aria-label="View courses"
className="text-xs font-medium text-[var(--text-dark)] hover:underline"
>
{courses}
</Link>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<div className="flex h-5 w-5 items-center justify-center rounded-sm bg-[var(--sidebar-icon-bg)]">
<FileText className="h-3 w-3 text-[var(--sidebar-icon-color)]" />
<Award className="h-3 w-3 text-[var(--sidebar-icon-color)]" />
</div>
<span className="text-xs text-[var(--sidebar-text)]">Assessments</span>
<span className="text-xs text-[var(--sidebar-text)]">Credentials</span>
</div>
<span className="text-xs font-medium text-[var(--text-dark)]">{total_assessments}</span>
<Link
href={`/platform/${tenant}/profile/credentials`}
aria-label="View credentials"
className="text-xs font-medium text-[var(--text-dark)] hover:underline"
>
{credentials}
</Link>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<div className="flex h-5 w-5 items-center justify-center rounded-sm bg-[var(--sidebar-icon-bg)]">
<Award className="h-3 w-3 text-[var(--sidebar-icon-color)]" />
<Sparkles className="h-3 w-3 text-[var(--sidebar-icon-color)]" />
</div>
<span className="text-xs text-[var(--sidebar-text)]">Courses Completions</span>
<span className="text-xs text-[var(--sidebar-text)]">Skills</span>
</div>
<span className="text-xs font-medium text-[var(--text-dark)]">{course_completions}</span>
<Link
href={`/platform/${tenant}/profile/skills`}
aria-label="View skills"
className="text-xs font-medium text-[var(--text-dark)] hover:underline"
>
{skills}
</Link>
</div>
</div>
</div>
Expand Down
4 changes: 0 additions & 4 deletions components/credentials-list-box.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import { Plus } from 'lucide-react';
import { Assertion } from '@iblai/iblai-api';
import { CredentialMiniBox } from './credential-mini-box';
import { DefaultEmptyBox } from './default-empty-box';
import Link from 'next/link';
import { useTenantParam } from '@/hooks/use-tenant-param';
type CredentialsListBoxProps = {
Expand All @@ -27,9 +26,6 @@ export function CredentialsListBox({ credentials }: CredentialsListBoxProps) {
</span>
</Link>
</div>
{credentials.length === 0 && (
<DefaultEmptyBox className="w-full" message="No credentials yet." />
)}
<div className="space-y-2">
{credentials.map((credential, index) => (
<CredentialMiniBox
Expand Down
8 changes: 5 additions & 3 deletions components/profile-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { AllTimePerLearnerBox } from './all-time-perlearner-box';
import { useUserMetadata } from '@/hooks/users/use-usermetadata';
import { UserProfileBox } from './user-profile-box';
import { useProfileCredentials } from '@/hooks/profile/use-profile-credentials';
import { useAllTimeStats } from '@/hooks/profile/use-all-time-stats';

export function ProfileSidebar() {
const { latestSkills, latestSkillsLoading } = useLatestSkills(6);
Expand All @@ -23,6 +24,7 @@ export function ProfileSidebar() {
});
const { userPerLearnerInfo, userPerLearnerInfoLoading } = usePerLearnerInfoQuery();
const { userMetaDataLoading } = useUserMetadata();
const { courses, credentials: credentialsCount, skills } = useAllTimeStats();

return (
<aside
Expand All @@ -46,10 +48,10 @@ export function ProfileSidebar() {
<AllTimeSkeleton />
) : (
<AllTimePerLearnerBox
total_assessments={userPerLearnerInfo?.total_assessments ?? 0}
total_time_spent={userPerLearnerInfo?.total_time_spent ?? 0}
total_videos={userPerLearnerInfo?.total_videos ?? 0}
course_completions={userPerLearnerInfo?.course_completions ?? 0}
courses={courses}
credentials={credentialsCount}
skills={skills}
/>
)}
</aside>
Expand Down
Loading
Loading