diff --git a/apps/frontend/src/components/deployments/AnalyticsCard.test.tsx b/apps/frontend/src/components/deployments/AnalyticsCard.test.tsx new file mode 100644 index 0000000..455bf50 --- /dev/null +++ b/apps/frontend/src/components/deployments/AnalyticsCard.test.tsx @@ -0,0 +1,50 @@ +import { describe, it, expect } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import { AnalyticsCard } from './AnalyticsCard'; + +const icon = ; + +describe('AnalyticsCard', () => { + it('renders label and value', () => { + render(); + expect(screen.getByText('Total Deployments')).toBeDefined(); + expect(screen.getByText('248')).toBeDefined(); + }); + + it('renders subValue when provided', () => { + render(); + expect(screen.getByText('5 today')).toBeDefined(); + }); + + it('does not render subValue when omitted', () => { + render(); + expect(screen.queryByText('5 today')).toBeNull(); + }); + + it('renders positive trend with + sign', () => { + render(); + expect(screen.getByText(/\+2\.1/)).toBeDefined(); + }); + + it('renders negative trend without + sign', () => { + render(); + expect(screen.getByText(/-8/)).toBeDefined(); + expect(screen.queryByText(/\+/)).toBeNull(); + }); + + it('does not render trend badge when trend is undefined', () => { + const { container } = render(); + expect(container.querySelector('.bg-green-50')).toBeNull(); + expect(container.querySelector('.bg-red-50')).toBeNull(); + }); + + it('has accessible article with aria-label', () => { + render(); + expect(screen.getByRole('article', { name: 'Success Rate' })).toBeDefined(); + }); + + it('sets the id attribute', () => { + const { container } = render(); + expect(container.querySelector('#analytics-total')).not.toBeNull(); + }); +}); diff --git a/apps/frontend/src/components/deployments/AnalyticsGrid.test.tsx b/apps/frontend/src/components/deployments/AnalyticsGrid.test.tsx new file mode 100644 index 0000000..607581b --- /dev/null +++ b/apps/frontend/src/components/deployments/AnalyticsGrid.test.tsx @@ -0,0 +1,62 @@ +import { describe, it, expect } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import { AnalyticsGrid } from './AnalyticsGrid'; +import type { DeploymentAnalytics } from '@/types/deployment'; + +const analytics: DeploymentAnalytics = { + totalDeployments: 248, + successRate: 94.2, + avgDurationSeconds: 118, + activeDeployments: 2, + failedLast24h: 1, + deploymentsToday: 8, + successRateTrend: 2.1, + avgDurationTrend: -8, +}; + +describe('AnalyticsGrid', () => { + it('renders all six analytics cards', () => { + render(); + expect(screen.getByText('Total Deployments')).toBeDefined(); + expect(screen.getByText('Success Rate')).toBeDefined(); + expect(screen.getByText('Avg Build Time')).toBeDefined(); + expect(screen.getByText('Active Deployments')).toBeDefined(); + expect(screen.getByText('Failed (24 h)')).toBeDefined(); + expect(screen.getByText('Deployments Today')).toBeDefined(); + }); + + it('displays total deployments value', () => { + render(); + expect(screen.getByText('248')).toBeDefined(); + }); + + it('displays success rate formatted to 1 decimal', () => { + render(); + expect(screen.getByText('94.2%')).toBeDefined(); + }); + + it('displays avg duration in minutes and seconds', () => { + render(); + expect(screen.getByText('1m 58s')).toBeDefined(); + }); + + it('displays avg duration in seconds only when < 60s', () => { + render(); + expect(screen.getByText('45s')).toBeDefined(); + }); + + it('has accessible section label', () => { + render(); + expect(screen.getByRole('region', { name: 'Deployment analytics' })).toBeDefined(); + }); + + it('shows "No failures" sub-value when failedLast24h is 0', () => { + render(); + expect(screen.getByText('No failures β€” great!')).toBeDefined(); + }); + + it('shows "Needs attention" sub-value when failedLast24h > 0', () => { + render(); + expect(screen.getByText('Needs attention')).toBeDefined(); + }); +}); diff --git a/apps/frontend/src/components/deployments/DeploymentRow.test.tsx b/apps/frontend/src/components/deployments/DeploymentRow.test.tsx new file mode 100644 index 0000000..3f201be --- /dev/null +++ b/apps/frontend/src/components/deployments/DeploymentRow.test.tsx @@ -0,0 +1,106 @@ +import { describe, it, expect, vi } from 'vitest'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { DeploymentRow } from './DeploymentRow'; +import type { Deployment } from '@/types/deployment'; + +const base: Deployment = { + id: 'dep-001', + name: 'stellar-dex', + status: 'success', + environment: 'production', + trigger: 'push', + commit: { + sha: 'a3f9c12', + message: 'feat: add liquidity pool', + author: 'jana.m', + branch: 'main', + }, + region: { id: 'us-east-1', label: 'US East', flag: 'πŸ‡ΊπŸ‡Έ' }, + createdAt: new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString(), + durationSeconds: 127, + url: 'https://stellar-dex.craft.app', + logsUrl: '#', +}; + +describe('DeploymentRow', () => { + it('renders deployment name', () => { + render(
); + expect(screen.getByText('stellar-dex')).toBeDefined(); + }); + + it('renders commit sha and message', () => { + render(
); + expect(screen.getByText('a3f9c12')).toBeDefined(); + expect(screen.getByText('feat: add liquidity pool')).toBeDefined(); + }); + + it('renders environment badge', () => { + render(
); + expect(screen.getByText('Production')).toBeDefined(); + }); + + it('renders status badge', () => { + render(
); + expect(screen.getByText('Success')).toBeDefined(); + }); + + it('renders duration when provided', () => { + render(
); + expect(screen.getByText('2m 7s')).toBeDefined(); + }); + + it('does not render duration when omitted', () => { + const { queryByText } = render(
); + expect(queryByText(/\dm/)).toBeNull(); + }); + + it('renders visit link when url is present', () => { + render(
); + const link = screen.getByRole('link', { name: /Visit stellar-dex/i }); + expect(link.getAttribute('href')).toBe('https://stellar-dex.craft.app'); + }); + + it('does not render visit link when url is absent', () => { + render(
); + expect(screen.queryByRole('link', { name: /Visit/i })).toBeNull(); + }); + + it('calls onViewLogs when logs button clicked', () => { + const onViewLogs = vi.fn(); + render(
); + fireEvent.click(screen.getByRole('button', { name: /View logs for stellar-dex/i })); + expect(onViewLogs).toHaveBeenCalledWith('dep-001'); + }); + + it('does not render logs button when logsUrl is absent', () => { + render(
); + expect(screen.queryByRole('button', { name: /View logs/i })).toBeNull(); + }); + + it('calls onRedeploy when redeploy button clicked for success status', () => { + const onRedeploy = vi.fn(); + render(
); + fireEvent.click(screen.getByRole('button', { name: /Redeploy stellar-dex/i })); + expect(onRedeploy).toHaveBeenCalledWith('dep-001'); + }); + + it('renders redeploy button for failed status', () => { + render(
); + expect(screen.getByRole('button', { name: /Redeploy/i })).toBeDefined(); + }); + + it('does not render redeploy button for running status', () => { + render(
); + expect(screen.queryByRole('button', { name: /Redeploy/i })).toBeNull(); + }); + + it('sets the row id attribute', () => { + const { container } = render(
); + expect(container.querySelector('#deployment-row-dep-001')).not.toBeNull(); + }); + + it('renders region flag', () => { + render(
); + expect(screen.getByText('πŸ‡ΊπŸ‡Έ')).toBeDefined(); + }); +});