Skip to content
Open
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
44 changes: 44 additions & 0 deletions app/src/components/channels/mcp/McpStatusBadge.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/**
* Tests for McpStatusBadge — renders the i18n'd label and a11y role
* for each ServerStatus, and forwards custom className.
*/
import { render, screen } from '@testing-library/react';
import { describe, expect, it } from 'vitest';

import McpStatusBadge from './McpStatusBadge';
import type { ServerStatus } from './types';

describe('McpStatusBadge', () => {
it.each<[ServerStatus, string]>([
['connected', 'Connected'],
['connecting', 'Connecting'],
['disconnected', 'Disconnected'],
['error', 'Error'],
])('renders i18n label for status=%s', (status, expectedLabel) => {
render(<McpStatusBadge status={status} />);
expect(screen.getByRole('status')).toHaveTextContent(expectedLabel);
});

it('exposes role="status" and aria-live="polite" for assistive tech', () => {
render(<McpStatusBadge status="connecting" />);
const badge = screen.getByRole('status');
expect(badge).toHaveAttribute('aria-live', 'polite');
});

it('falls back to the disconnected style for an unknown status value', () => {
// ServerStatus is a closed union, but the runtime fallback exists for
// forward-compat with possible future Rust-side variants — exercise it.
render(<McpStatusBadge status={'spawning' as ServerStatus} />);
expect(screen.getByRole('status')).toHaveTextContent('Disconnected');
});

it('appends the optional className without dropping the built-in classes', () => {
render(<McpStatusBadge status="connected" className="ml-2 my-custom-class" />);
const badge = screen.getByRole('status');
expect(badge.className).toContain('my-custom-class');
expect(badge.className).toContain('ml-2');
// Built-in look-and-feel preserved.
expect(badge.className).toContain('rounded-full');
expect(badge.className).toContain('bg-sage-500/10');
});
});
24 changes: 15 additions & 9 deletions app/src/components/channels/mcp/McpStatusBadge.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,28 @@
/**
* Status badge for MCP server connection states.
* Mirrors ChannelStatusBadge but uses ServerStatus values.
* Mirrors ChannelStatusBadge but uses ServerStatus values; reuses the
* shared `channels.status.*` i18n keys since the label vocabulary is
* identical (Connected / Connecting / Disconnected / Error).
*/
import { useT } from '../../../lib/i18n/I18nContext';
import type { ServerStatus } from './types';

const STATUS_STYLES: Record<ServerStatus, { label: string; className: string }> = {
const STATUS_META: Record<ServerStatus, { i18nKey: string; className: string }> = {
connected: {
label: 'Connected',
i18nKey: 'channels.status.connected',
className: 'bg-sage-500/10 text-sage-700 border-sage-500/30 dark:text-sage-300',
},
connecting: {
label: 'Connecting',
i18nKey: 'channels.status.connecting',
className: 'bg-amber-500/10 text-amber-700 border-amber-500/30 dark:text-amber-300',
},
disconnected: {
label: 'Disconnected',
i18nKey: 'channels.status.disconnected',
className:
'bg-stone-100 dark:bg-neutral-800 text-stone-500 dark:text-neutral-400 border-stone-200 dark:border-neutral-700',
},
error: {
label: 'Error',
i18nKey: 'channels.status.error',
className: 'bg-coral-500/10 text-coral-700 border-coral-500/30 dark:text-coral-300',
},
};
Expand All @@ -30,11 +33,14 @@ interface McpStatusBadgeProps {
}

const McpStatusBadge = ({ status, className = '' }: McpStatusBadgeProps) => {
const style = STATUS_STYLES[status] ?? STATUS_STYLES.disconnected;
const { t } = useT();
const meta = STATUS_META[status] ?? STATUS_META.disconnected;
return (
<span
className={`shrink-0 px-2 py-1 text-[11px] border rounded-full ${style.className} ${className}`}>
{style.label}
role="status"
aria-live="polite"
className={`shrink-0 px-2 py-1 text-[11px] border rounded-full ${meta.className} ${className}`}>
{t(meta.i18nKey)}
</span>
);
};
Expand Down
Loading