import { render, screen, fireEvent } from '@testing-library/react' import { MetricCard } from '../MetricCard' import { PipelineFunnel } from '../PipelineFunnel' import { TopCampaigns } from '../TopCampaigns' import { SparklineTrend } from '../SparklineTrend' import { PeriodSelector } from '../PeriodSelector' import { DashboardGrid } from '../DashboardGrid' import { DashboardSection } from '../DashboardSection' // --------------------------------------------------------------------------- // Shared test helpers // --------------------------------------------------------------------------- function MockIcon({ className }: { className?: string }) { return } // --------------------------------------------------------------------------- // MetricCard // --------------------------------------------------------------------------- describe('MetricCard', () => { const baseProps = { label: 'Total Leads', value: 1234, icon: MockIcon, } it('renders the label and value', () => { render() expect(screen.getByText('Total Leads')).toBeInTheDocument() expect(screen.getByText('1234')).toBeInTheDocument() }) it('renders string values', () => { render() expect(screen.getByText('$9,999')).toBeInTheDocument() }) it('renders the icon', () => { render() expect(screen.getByTestId('mock-icon')).toBeInTheDocument() }) it('renders "—" while loading', () => { render() expect(screen.getByText('—')).toBeInTheDocument() expect(screen.queryByText('1234')).not.toBeInTheDocument() }) it('renders "Error" in error state', () => { render() expect(screen.getByText('Error')).toBeInTheDocument() }) it('applies animate-pulse class when loading', () => { const { container } = render() expect(container.firstChild).toHaveClass('animate-pulse') }) it('applies error border classes when error is true', () => { const { container } = render() // The inner content div gets the error classes const cardDiv = container.querySelector('.border-red-200') expect(cardDiv).toBeInTheDocument() }) describe('trend indicator', () => { it('renders positive trend value with + prefix', () => { render() expect(screen.getByText('+12%')).toBeInTheDocument() }) it('renders negative trend value without + prefix', () => { render() expect(screen.getByText('-5%')).toBeInTheDocument() }) it('renders neutral trend when isPositive is undefined', () => { render() expect(screen.getByText('0%')).toBeInTheDocument() }) it('renders positive trend in green', () => { render() const trendEl = screen.getByText('+8%').closest('div') expect(trendEl).toHaveClass('text-green-600') }) it('renders negative trend in red', () => { render() const trendEl = screen.getByText('-3%').closest('div') expect(trendEl).toHaveClass('text-red-600') }) it('renders neutral trend in gray', () => { render() const trendEl = screen.getByText('0%').closest('div') expect(trendEl).toHaveClass('text-gray-500') }) it('renders the trend label', () => { render( ) expect(screen.getByText('vs last month')).toBeInTheDocument() }) it('hides trend while loading', () => { render( ) expect(screen.queryByText('+5%')).not.toBeInTheDocument() }) it('hides trend in error state', () => { render( ) expect(screen.queryByText('+5%')).not.toBeInTheDocument() }) }) describe('sparkline', () => { it('renders sparkline when sparklineData has 2+ points', () => { const { container } = render( ) expect(container.querySelector('svg')).toBeInTheDocument() }) it('does not render sparkline for a single data point', () => { const { container } = render( ) // The only svg present might be the icon; sparkline itself returns null for < 2 points const svgs = container.querySelectorAll('svg') // icon is an svg; sparkline should not add a second one expect(svgs.length).toBe(1) }) it('does not render sparkline while loading', () => { const { container } = render( ) // Only the mock icon svg should exist (no sparkline) const svgs = container.querySelectorAll('svg') expect(svgs.length).toBe(1) }) }) describe('href link', () => { it('wraps content in an anchor tag when href is provided', () => { render() const link = screen.getByRole('link') expect(link).toHaveAttribute('href', '/leads') }) it('sets accessible label on the anchor', () => { render() expect(screen.getByRole('link', { name: 'View Total Leads details' })).toBeInTheDocument() }) it('does not render anchor when href is omitted', () => { render() expect(screen.queryByRole('link')).not.toBeInTheDocument() }) }) it('applies custom className', () => { const { container } = render() const cardDiv = container.querySelector('.custom-class') expect(cardDiv).toBeInTheDocument() }) }) // --------------------------------------------------------------------------- // SparklineTrend // --------------------------------------------------------------------------- describe('SparklineTrend', () => { it('renders an svg with the correct role and aria-label for increasing trend', () => { render() const svg = screen.getByRole('img') expect(svg).toHaveAttribute('aria-label', 'Trend chart showing increasing pattern') }) it('reports decreasing trend in aria-label', () => { render() expect(screen.getByRole('img')).toHaveAttribute( 'aria-label', 'Trend chart showing decreasing pattern' ) }) it('reports stable trend in aria-label', () => { render() expect(screen.getByRole('img')).toHaveAttribute( 'aria-label', 'Trend chart showing stable pattern' ) }) it('returns null for a single data point', () => { const { container } = render() expect(container.firstChild).toBeNull() }) it('returns null for an empty array', () => { const { container } = render() expect(container.firstChild).toBeNull() }) it('renders both the line path and the area fill path', () => { const { container } = render() const paths = container.querySelectorAll('path') // area fill + line stroke = 2 paths expect(paths.length).toBe(2) }) it('accepts a hex color directly', () => { const { container } = render() const linePath = container.querySelectorAll('path')[1] expect(linePath).toHaveAttribute('stroke', '#FF0000') }) it('resolves a Tailwind color class to hex', () => { const { container } = render() const linePath = container.querySelectorAll('path')[1] expect(linePath).toHaveAttribute('stroke', '#10B981') }) it('falls back to blue-500 for unknown color keys', () => { const { container } = render() const linePath = container.querySelectorAll('path')[1] expect(linePath).toHaveAttribute('stroke', '#3B82F6') }) it('applies custom height', () => { const { container } = render() const svg = container.querySelector('svg') expect(svg).toHaveAttribute('height', '60') }) it('applies custom className', () => { const { container } = render( ) expect(container.querySelector('svg')).toHaveClass('my-sparkline') }) }) // --------------------------------------------------------------------------- // PeriodSelector // --------------------------------------------------------------------------- describe('PeriodSelector', () => { const onChange = jest.fn() beforeEach(() => jest.clearAllMocks()) it('renders default period buttons', () => { render() expect(screen.getByText('Week')).toBeInTheDocument() expect(screen.getByText('Month')).toBeInTheDocument() expect(screen.getByText('Quarter')).toBeInTheDocument() expect(screen.getByText('Year')).toBeInTheDocument() }) it('marks the selected period button as pressed', () => { render() expect(screen.getByText('Quarter')).toHaveAttribute('aria-pressed', 'true') expect(screen.getByText('Week')).toHaveAttribute('aria-pressed', 'false') expect(screen.getByText('Month')).toHaveAttribute('aria-pressed', 'false') expect(screen.getByText('Year')).toHaveAttribute('aria-pressed', 'false') }) it('calls onChange with the correct value on button click', () => { render() fireEvent.click(screen.getByText('Year')) expect(onChange).toHaveBeenCalledWith('year') expect(onChange).toHaveBeenCalledTimes(1) }) it('calls onChange when the already-selected period is clicked', () => { render() fireEvent.click(screen.getByText('Month')) expect(onChange).toHaveBeenCalledWith('month') }) it('renders custom periods', () => { const custom = [ { value: 'day', label: 'Day' }, { value: 'week', label: 'Week' }, ] render() expect(screen.getByText('Day')).toBeInTheDocument() expect(screen.getByText('Week')).toBeInTheDocument() expect(screen.queryByText('Month')).not.toBeInTheDocument() }) it('has role="group" with accessible label', () => { const { container } = render() const group = container.querySelector('[role="group"]') expect(group).toBeInTheDocument() expect(group).toHaveAttribute('aria-label', 'Time period') }) it('applies custom className to the wrapper', () => { const { container } = render( ) expect(container.firstChild).toHaveClass('my-selector') }) it('applies active style class only to selected button', () => { render() const weekBtn = screen.getByText('Week') const monthBtn = screen.getByText('Month') expect(weekBtn).toHaveClass('bg-primary/15') expect(monthBtn).not.toHaveClass('bg-primary/15') }) }) // --------------------------------------------------------------------------- // PipelineFunnel // --------------------------------------------------------------------------- describe('PipelineFunnel', () => { const stages = [ { stage: 'Lead', count: 200 }, { stage: 'Qualified', count: 100 }, { stage: 'Proposal', count: 50 }, { stage: 'Closed', count: 20 }, ] it('renders the default title', () => { render() expect(screen.getByText('Pipeline Funnel')).toBeInTheDocument() }) it('renders a custom title', () => { render() expect(screen.getByText('Sales Funnel')).toBeInTheDocument() }) it('renders all stage names', () => { render() expect(screen.getByText('Lead')).toBeInTheDocument() expect(screen.getByText('Qualified')).toBeInTheDocument() expect(screen.getByText('Proposal')).toBeInTheDocument() expect(screen.getByText('Closed')).toBeInTheDocument() }) it('renders stage counts with locale formatting', () => { const bigStages = [ { stage: 'Top', count: 1000 }, { stage: 'Mid', count: 500 }, ] render() expect(screen.getByText('1,000')).toBeInTheDocument() expect(screen.getByText('500')).toBeInTheDocument() }) it('shows "% of total" relative to the first stage', () => { render() // First stage = 200, second = 100 → 50%, third = 50 → 25%, last = 20 → 10% expect(screen.getByText('100% of total')).toBeInTheDocument() expect(screen.getByText('50% of total')).toBeInTheDocument() expect(screen.getByText('25% of total')).toBeInTheDocument() expect(screen.getByText('10% of total')).toBeInTheDocument() }) it('renders the overall conversion footer when there are 2+ stages', () => { render() expect(screen.getByText('Overall Conversion')).toBeInTheDocument() // The footer span contains "10%" directly followed by the route span "(Lead → Closed)" // Use getAllByText and check one matches the bold footer element specifically const tenPctMatches = screen.getAllByText(/10%/) // At least one match must be the bold footer span (not the "% of total" span) const footerMatch = tenPctMatches.find( el => !el.textContent?.includes('of total') ) expect(footerMatch).toBeTruthy() expect(screen.getByText(/Lead → Closed/)).toBeInTheDocument() }) it('does not render overall conversion footer with a single stage', () => { render() expect(screen.queryByText('Overall Conversion')).not.toBeInTheDocument() }) it('renders empty state when stages is an empty array', () => { render() expect(screen.getByText('No pipeline data available')).toBeInTheDocument() }) it('renders empty state title even in empty state', () => { render() expect(screen.getByText('My Funnel')).toBeInTheDocument() }) it('renders stage list with role="list"', () => { render() expect(screen.getByRole('list', { name: 'Pipeline stages' })).toBeInTheDocument() }) it('renders each stage as a listitem', () => { render() expect(screen.getAllByRole('listitem')).toHaveLength(stages.length) }) it('renders bar buttons with accessible labels', () => { render() const leadBtn = screen.getByRole('button', { name: 'Lead: 200 prospects, 100% of total', }) expect(leadBtn).toBeInTheDocument() }) it('calls onStageClick with stage name when a bar is clicked', () => { const onStageClick = jest.fn() render() // Click the listitem div wrapping the 'Qualified' stage const listitems = screen.getAllByRole('listitem') fireEvent.click(listitems[1]) expect(onStageClick).toHaveBeenCalledWith('Qualified') expect(onStageClick).toHaveBeenCalledTimes(1) }) it('disables bar buttons when onStageClick is not provided', () => { render() const buttons = screen.getAllByRole('button') buttons.forEach(btn => expect(btn).toBeDisabled()) }) it('enables bar buttons when onStageClick is provided', () => { render() const buttons = screen.getAllByRole('button') buttons.forEach(btn => expect(btn).not.toBeDisabled()) }) it('handles stages with zero counts without crashing', () => { const zeroStages = [ { stage: 'Lead', count: 0 }, { stage: 'Closed', count: 0 }, ] render() expect(screen.getByText('Lead')).toBeInTheDocument() expect(screen.getByText('Overall Conversion')).toBeInTheDocument() }) }) // --------------------------------------------------------------------------- // TopCampaigns // --------------------------------------------------------------------------- describe('TopCampaigns', () => { const campaigns = [ { id: '1', name: 'Spring Launch', engagementRate: 80, status: 'active' as const }, { id: '2', name: 'Summer Sale', engagementRate: 55, status: 'paused' as const }, { id: '3', name: 'Fall Promo', engagementRate: 30, status: 'completed' as const }, { id: '4', name: 'Winter Deal', engagementRate: 15 }, { id: '5', name: 'Holiday Push', engagementRate: 70, status: 'active' as const }, { id: '6', name: 'Extra Campaign', engagementRate: 40, status: 'active' as const }, ] it('renders the default title', () => { render() expect(screen.getByText('Top Performing Campaigns')).toBeInTheDocument() }) it('renders a custom title', () => { render() expect(screen.getByText('Best Campaigns')).toBeInTheDocument() }) it('shows empty state when campaigns array is empty', () => { render() expect(screen.getByText('No campaigns yet')).toBeInTheDocument() expect( screen.getByText('Launch your first campaign to see performance metrics') ).toBeInTheDocument() }) it('renders empty state title in empty state', () => { render() expect(screen.getByText('My Campaigns')).toBeInTheDocument() }) it('limits display to maxItems (default 5)', () => { render() // 6 campaigns but only 5 should appear expect(screen.queryByText('Extra Campaign')).not.toBeInTheDocument() expect(screen.getByText('Spring Launch')).toBeInTheDocument() expect(screen.getByText('Holiday Push')).toBeInTheDocument() }) it('respects a custom maxItems value', () => { render() expect(screen.getByText('Spring Launch')).toBeInTheDocument() expect(screen.getByText('Summer Sale')).toBeInTheDocument() expect(screen.queryByText('Fall Promo')).not.toBeInTheDocument() }) it('renders rank numbers starting at 1', () => { render() expect(screen.getByText('1')).toBeInTheDocument() expect(screen.getByText('2')).toBeInTheDocument() expect(screen.getByText('3')).toBeInTheDocument() }) it('renders engagement rates as percentages', () => { render() expect(screen.getByText('80%')).toBeInTheDocument() expect(screen.getByText('55%')).toBeInTheDocument() }) it('renders progress bars with correct aria attributes', () => { render() const progressBars = screen.getAllByRole('progressbar') expect(progressBars[0]).toHaveAttribute('aria-valuenow', '80') expect(progressBars[0]).toHaveAttribute('aria-valuemin', '0') expect(progressBars[0]).toHaveAttribute('aria-valuemax', '100') expect(progressBars[0]).toHaveAttribute( 'aria-label', 'Spring Launch engagement rate: 80%' ) }) it('renders campaign status badges', () => { render() // Two campaigns have 'active' status within the first 5 (Spring Launch + Holiday Push) const activebadges = screen.getAllByText('active') expect(activebadges.length).toBeGreaterThanOrEqual(1) expect(screen.getByText('paused')).toBeInTheDocument() expect(screen.getByText('completed')).toBeInTheDocument() }) it('does not render a status badge when status is omitted', () => { const noBadge = [{ id: '1', name: 'No Status', engagementRate: 50 }] render() // Only one listitem, no status span expected expect(screen.queryByText('active')).not.toBeInTheDocument() expect(screen.queryByText('paused')).not.toBeInTheDocument() }) it('renders the list with role and aria-label', () => { render() expect( screen.getByRole('list', { name: 'Top campaigns by engagement' }) ).toBeInTheDocument() }) it('renders each campaign as a listitem', () => { render() // maxItems defaults to 5 expect(screen.getAllByRole('listitem')).toHaveLength(5) }) it('shows average engagement footer', () => { const simple = [ { id: '1', name: 'A', engagementRate: 40 }, { id: '2', name: 'B', engagementRate: 60 }, ] render() expect(screen.getByText('Average Engagement')).toBeInTheDocument() expect(screen.getByText('50%')).toBeInTheDocument() }) describe('View all button', () => { it('shows "View all" when onViewAll is provided AND campaigns exceed maxItems', () => { render( ) expect(screen.getByText('View all')).toBeInTheDocument() }) it('does not show "View all" when campaigns do not exceed maxItems', () => { render( ) expect(screen.queryByText('View all')).not.toBeInTheDocument() }) it('does not show "View all" when onViewAll is not provided', () => { render() expect(screen.queryByText('View all')).not.toBeInTheDocument() }) it('calls onViewAll when "View all" button is clicked', () => { const onViewAll = jest.fn() render() fireEvent.click(screen.getByText('View all')) expect(onViewAll).toHaveBeenCalledTimes(1) }) }) describe('progress bar color thresholds', () => { it('applies success color for engagement >= 75', () => { const { container } = render( ) const bar = container.querySelector('[role="progressbar"]') expect(bar).toHaveClass('bg-success-500') }) it('applies primary color for engagement >= 50 and < 75', () => { const { container } = render( ) const bar = container.querySelector('[role="progressbar"]') expect(bar).toHaveClass('bg-primary') }) it('applies warning color for engagement >= 25 and < 50', () => { const { container } = render( ) const bar = container.querySelector('[role="progressbar"]') expect(bar).toHaveClass('bg-warning-500') }) it('applies gray color for engagement < 25', () => { const { container } = render( ) const bar = container.querySelector('[role="progressbar"]') expect(bar).toHaveClass('bg-gray-400') }) }) }) // --------------------------------------------------------------------------- // DashboardGrid // --------------------------------------------------------------------------- describe('DashboardGrid', () => { it('renders children', () => { render(
A
B
) expect(screen.getByTestId('child-a')).toBeInTheDocument() expect(screen.getByTestId('child-b')).toBeInTheDocument() }) it('defaults to 2 columns and md gap', () => { const { container } = render(
) const grid = container.firstChild as HTMLElement expect(grid).toHaveClass('lg:grid-cols-2') expect(grid).toHaveClass('gap-6') }) it('applies lg:grid-cols-1 when columns=1', () => { const { container } = render(
) expect(container.firstChild).toHaveClass('lg:grid-cols-1') }) it('applies lg:grid-cols-3 when columns=3', () => { const { container } = render(
) expect(container.firstChild).toHaveClass('lg:grid-cols-3') }) it('applies sm:grid-cols-2 and lg:grid-cols-4 when columns=4', () => { const { container } = render(
) const grid = container.firstChild as HTMLElement expect(grid).toHaveClass('sm:grid-cols-2') expect(grid).toHaveClass('lg:grid-cols-4') }) it('applies gap-4 for gap="sm"', () => { const { container } = render(
) expect(container.firstChild).toHaveClass('gap-4') }) it('applies gap-8 for gap="lg"', () => { const { container } = render(
) expect(container.firstChild).toHaveClass('gap-8') }) it('always has grid-cols-1 as the base class', () => { const { container } = render(
) expect(container.firstChild).toHaveClass('grid-cols-1') }) it('applies a custom className', () => { const { container } = render(
) expect(container.firstChild).toHaveClass('my-grid') }) }) // --------------------------------------------------------------------------- // DashboardSection // --------------------------------------------------------------------------- describe('DashboardSection', () => { it('renders the title', () => { render(
) expect(screen.getByText('Overview')).toBeInTheDocument() }) it('renders children', () => { render(

Content here

) expect(screen.getByTestId('content')).toBeInTheDocument() }) it('renders description when provided', () => { render(
) expect(screen.getByText('A summary of key metrics')).toBeInTheDocument() }) it('does not render description when omitted', () => { const { container } = render(
) expect(container.querySelector('p')).not.toBeInTheDocument() }) it('renders action node when provided', () => { render( Export} >
) expect(screen.getByTestId('action-btn')).toBeInTheDocument() }) it('does not render the action wrapper when action is omitted', () => { const { container } = render(
) // The action wrapper div has class flex-shrink-0 expect(container.querySelector('.flex-shrink-0')).not.toBeInTheDocument() }) it('renders the title as an h2 element', () => { render(
) expect(screen.getByRole('heading', { level: 2, name: 'My Section' })).toBeInTheDocument() }) it('applies custom className', () => { const { container } = render(
) expect(container.firstChild).toHaveClass('my-section') }) it('still renders correctly with all props provided', () => { render( Action} className="full-class" > Slot content ) expect(screen.getByText('Full Section')).toBeInTheDocument() expect(screen.getByText('Detailed description')).toBeInTheDocument() expect(screen.getByText('Action')).toBeInTheDocument() expect(screen.getByTestId('slot')).toBeInTheDocument() }) })