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
50 changes: 50 additions & 0 deletions apps/frontend/src/components/deployments/AnalyticsCard.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import { AnalyticsCard } from './AnalyticsCard';

const icon = <svg aria-hidden="true" />;

describe('AnalyticsCard', () => {
it('renders label and value', () => {
render(<AnalyticsCard id="c1" label="Total Deployments" value="248" icon={icon} />);
expect(screen.getByText('Total Deployments')).toBeDefined();
expect(screen.getByText('248')).toBeDefined();
});

it('renders subValue when provided', () => {
render(<AnalyticsCard id="c1" label="Total" value="10" subValue="5 today" icon={icon} />);
expect(screen.getByText('5 today')).toBeDefined();
});

it('does not render subValue when omitted', () => {
render(<AnalyticsCard id="c1" label="Total" value="10" icon={icon} />);
expect(screen.queryByText('5 today')).toBeNull();
});

it('renders positive trend with + sign', () => {
render(<AnalyticsCard id="c1" label="Rate" value="94%" trend={2.1} trendLabel="pp" icon={icon} />);
expect(screen.getByText(/\+2\.1/)).toBeDefined();
});

it('renders negative trend without + sign', () => {
render(<AnalyticsCard id="c1" label="Duration" value="2m" trend={-8} trendLabel="s" icon={icon} />);
expect(screen.getByText(/-8/)).toBeDefined();
expect(screen.queryByText(/\+/)).toBeNull();
});

it('does not render trend badge when trend is undefined', () => {
const { container } = render(<AnalyticsCard id="c1" label="Total" value="10" icon={icon} />);
expect(container.querySelector('.bg-green-50')).toBeNull();
expect(container.querySelector('.bg-red-50')).toBeNull();
});

it('has accessible article with aria-label', () => {
render(<AnalyticsCard id="c1" label="Success Rate" value="94%" icon={icon} />);
expect(screen.getByRole('article', { name: 'Success Rate' })).toBeDefined();
});

it('sets the id attribute', () => {
const { container } = render(<AnalyticsCard id="analytics-total" label="Total" value="10" icon={icon} />);
expect(container.querySelector('#analytics-total')).not.toBeNull();
});
});
62 changes: 62 additions & 0 deletions apps/frontend/src/components/deployments/AnalyticsGrid.test.tsx
Original file line number Diff line number Diff line change
@@ -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(<AnalyticsGrid analytics={analytics} />);
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(<AnalyticsGrid analytics={analytics} />);
expect(screen.getByText('248')).toBeDefined();
});

it('displays success rate formatted to 1 decimal', () => {
render(<AnalyticsGrid analytics={analytics} />);
expect(screen.getByText('94.2%')).toBeDefined();
});

it('displays avg duration in minutes and seconds', () => {
render(<AnalyticsGrid analytics={analytics} />);
expect(screen.getByText('1m 58s')).toBeDefined();
});

it('displays avg duration in seconds only when < 60s', () => {
render(<AnalyticsGrid analytics={{ ...analytics, avgDurationSeconds: 45 }} />);
expect(screen.getByText('45s')).toBeDefined();
});

it('has accessible section label', () => {
render(<AnalyticsGrid analytics={analytics} />);
expect(screen.getByRole('region', { name: 'Deployment analytics' })).toBeDefined();
});

it('shows "No failures" sub-value when failedLast24h is 0', () => {
render(<AnalyticsGrid analytics={{ ...analytics, failedLast24h: 0 }} />);
expect(screen.getByText('No failures — great!')).toBeDefined();
});

it('shows "Needs attention" sub-value when failedLast24h > 0', () => {
render(<AnalyticsGrid analytics={analytics} />);
expect(screen.getByText('Needs attention')).toBeDefined();
});
});
106 changes: 106 additions & 0 deletions apps/frontend/src/components/deployments/DeploymentRow.test.tsx
Original file line number Diff line number Diff line change
@@ -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(<ul><DeploymentRow deployment={base} /></ul>);
expect(screen.getByText('stellar-dex')).toBeDefined();
});

it('renders commit sha and message', () => {
render(<ul><DeploymentRow deployment={base} /></ul>);
expect(screen.getByText('a3f9c12')).toBeDefined();
expect(screen.getByText('feat: add liquidity pool')).toBeDefined();
});

it('renders environment badge', () => {
render(<ul><DeploymentRow deployment={base} /></ul>);
expect(screen.getByText('Production')).toBeDefined();
});

it('renders status badge', () => {
render(<ul><DeploymentRow deployment={base} /></ul>);
expect(screen.getByText('Success')).toBeDefined();
});

it('renders duration when provided', () => {
render(<ul><DeploymentRow deployment={base} /></ul>);
expect(screen.getByText('2m 7s')).toBeDefined();
});

it('does not render duration when omitted', () => {
const { queryByText } = render(<ul><DeploymentRow deployment={{ ...base, durationSeconds: undefined }} /></ul>);
expect(queryByText(/\dm/)).toBeNull();
});

it('renders visit link when url is present', () => {
render(<ul><DeploymentRow deployment={base} /></ul>);
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(<ul><DeploymentRow deployment={{ ...base, url: undefined }} /></ul>);
expect(screen.queryByRole('link', { name: /Visit/i })).toBeNull();
});

it('calls onViewLogs when logs button clicked', () => {
const onViewLogs = vi.fn();
render(<ul><DeploymentRow deployment={base} onViewLogs={onViewLogs} /></ul>);
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(<ul><DeploymentRow deployment={{ ...base, logsUrl: undefined }} onViewLogs={vi.fn()} /></ul>);
expect(screen.queryByRole('button', { name: /View logs/i })).toBeNull();
});

it('calls onRedeploy when redeploy button clicked for success status', () => {
const onRedeploy = vi.fn();
render(<ul><DeploymentRow deployment={base} onRedeploy={onRedeploy} /></ul>);
fireEvent.click(screen.getByRole('button', { name: /Redeploy stellar-dex/i }));
expect(onRedeploy).toHaveBeenCalledWith('dep-001');
});

it('renders redeploy button for failed status', () => {
render(<ul><DeploymentRow deployment={{ ...base, status: 'failed' }} onRedeploy={vi.fn()} /></ul>);
expect(screen.getByRole('button', { name: /Redeploy/i })).toBeDefined();
});

it('does not render redeploy button for running status', () => {
render(<ul><DeploymentRow deployment={{ ...base, status: 'running' }} onRedeploy={vi.fn()} /></ul>);
expect(screen.queryByRole('button', { name: /Redeploy/i })).toBeNull();
});

it('sets the row id attribute', () => {
const { container } = render(<ul><DeploymentRow deployment={base} /></ul>);
expect(container.querySelector('#deployment-row-dep-001')).not.toBeNull();
});

it('renders region flag', () => {
render(<ul><DeploymentRow deployment={base} /></ul>);
expect(screen.getByText('🇺🇸')).toBeDefined();
});
});