diff --git a/app/src/components/channels/mcp/McpStatusBadge.test.tsx b/app/src/components/channels/mcp/McpStatusBadge.test.tsx new file mode 100644 index 0000000000..2c3a840a2a --- /dev/null +++ b/app/src/components/channels/mcp/McpStatusBadge.test.tsx @@ -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(); + expect(screen.getByRole('status')).toHaveTextContent(expectedLabel); + }); + + it('exposes role="status" and aria-live="polite" for assistive tech', () => { + render(); + 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(); + expect(screen.getByRole('status')).toHaveTextContent('Disconnected'); + }); + + it('appends the optional className without dropping the built-in classes', () => { + render(); + 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'); + }); +}); diff --git a/app/src/components/channels/mcp/McpStatusBadge.tsx b/app/src/components/channels/mcp/McpStatusBadge.tsx index 586fb8fc1e..009c54c9ba 100644 --- a/app/src/components/channels/mcp/McpStatusBadge.tsx +++ b/app/src/components/channels/mcp/McpStatusBadge.tsx @@ -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 = { +const STATUS_META: Record = { 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', }, }; @@ -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 ( - {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)} ); };