diff --git a/tests/e2e/specs/registry.spec.ts b/tests/e2e/specs/registry.spec.ts
index d89e11e9..c286f80f 100644
--- a/tests/e2e/specs/registry.spec.ts
+++ b/tests/e2e/specs/registry.spec.ts
@@ -86,6 +86,80 @@ test.describe('Registry/Discover Page', () => {
});
});
+test.describe('Registry Server Icon Rendering', () => {
+ test('should render server icons as images not raw URLs', async ({ page }) => {
+ const dashboard = new DashboardPage(page);
+
+ await dashboard.navigate();
+ await page.locator('nav button:has-text("Discover")').click();
+
+ // Wait for content to load
+ await page.waitForTimeout(500);
+
+ // Server cards with URL icons should render img elements, not raw URL text
+ const serverIconImages = page.locator('[data-testid="server-icon-img"]');
+ const serverIconFallbacks = page.locator('[data-testid="server-icon-fallback"]');
+ const serverIconEmojis = page.locator('[data-testid="server-icon-emoji"]');
+
+ const imgCount = await serverIconImages.count();
+ const fallbackCount = await serverIconFallbacks.count();
+ const emojiCount = await serverIconEmojis.count();
+
+ // At least some icons should be rendered (either as img or fallback/emoji)
+ expect(imgCount + fallbackCount + emojiCount).toBeGreaterThan(0);
+
+ // Verify img elements have valid src attributes
+ if (imgCount > 0) {
+ const firstImg = serverIconImages.first();
+ const src = await firstImg.getAttribute('src');
+ expect(src).toMatch(/^https?:\/\//);
+ }
+
+ // Ensure no raw URL text is shown in place of icons
+ const cardTexts = await page.locator('[data-testid^="server-card-"]').allTextContents();
+ for (const text of cardTexts) {
+ expect(text).not.toMatch(/^https?:\/\/avatars\./);
+ }
+ });
+
+ test('should render icon as img in server detail modal', async ({ page }) => {
+ const dashboard = new DashboardPage(page);
+ const registry = new RegistryPage(page);
+
+ await dashboard.navigate();
+ await page.locator('nav button:has-text("Discover")').click();
+ await page.waitForTimeout(500);
+
+ // Click first server card to open detail modal
+ const firstCard = page.locator('[data-testid^="server-card-"]').first();
+ if (await firstCard.isVisible().catch(() => false)) {
+ await firstCard.click();
+ await page.waitForTimeout(300);
+
+ // The detail modal should render icons properly
+ const modalIconImg = page.locator('.fixed [data-testid="server-icon-img"]');
+ const modalIconFallback = page.locator('.fixed [data-testid="server-icon-fallback"]');
+ const modalIconEmoji = page.locator('.fixed [data-testid="server-icon-emoji"]');
+
+ const hasImg = await modalIconImg.isVisible().catch(() => false);
+ const hasFallback = await modalIconFallback.isVisible().catch(() => false);
+ const hasEmoji = await modalIconEmoji.isVisible().catch(() => false);
+
+ // At least one icon rendering approach should be used
+ expect(hasImg || hasFallback || hasEmoji).toBe(true);
+
+ // If img, verify it has valid src
+ if (hasImg) {
+ const src = await modalIconImg.getAttribute('src');
+ expect(src).toMatch(/^https?:\/\//);
+ }
+
+ // Close modal
+ await page.keyboard.press('Escape');
+ }
+ });
+});
+
test.describe('Registry Filters and Sorting', () => {
test('should have filter elements', async ({ page }) => {
const dashboard = new DashboardPage(page);
diff --git a/tests/ts/components/ServerCard.test.tsx b/tests/ts/components/ServerCard.test.tsx
new file mode 100644
index 00000000..aac8d228
--- /dev/null
+++ b/tests/ts/components/ServerCard.test.tsx
@@ -0,0 +1,130 @@
+import { describe, it, expect, vi } from 'vitest';
+import { render, screen, fireEvent } from '@testing-library/react';
+import { ServerCard } from '../../../apps/desktop/src/features/registry/ServerCard';
+import type { ServerViewModel } from '../../../apps/desktop/src/types/registry';
+
+function makeServer(overrides: Partial = {}): ServerViewModel {
+ return {
+ id: 'com.test-server',
+ name: 'Test Server',
+ description: 'A test MCP server',
+ alias: 'test',
+ icon: null,
+ auth: { type: 'none' },
+ transport: {
+ type: 'http',
+ url: 'https://example.com/mcp',
+ headers: {},
+ metadata: { inputs: [] },
+ },
+ categories: ['developer-tools'],
+ publisher: null,
+ source: { type: 'Registry', url: 'https://registry.mcpmux.com', name: 'McpMux Registry' },
+ is_installed: false,
+ enabled: false,
+ oauth_connected: false,
+ input_values: {},
+ connection_status: 'disconnected',
+ missing_required_inputs: false,
+ last_error: null,
+ ...overrides,
+ };
+}
+
+describe('ServerCard', () => {
+ const defaultProps = {
+ onInstall: vi.fn(),
+ onUninstall: vi.fn(),
+ onViewDetails: vi.fn(),
+ };
+
+ describe('icon rendering', () => {
+ it('should render fallback icon when icon is null', () => {
+ const server = makeServer({ icon: null });
+ render();
+ expect(screen.getByTestId('server-icon-fallback')).toHaveTextContent('📦');
+ });
+
+ it('should render emoji icon as text', () => {
+ const server = makeServer({ icon: '🔐' });
+ render();
+ expect(screen.getByTestId('server-icon-emoji')).toHaveTextContent('🔐');
+ });
+
+ it('should render URL icon as img element', () => {
+ const server = makeServer({
+ icon: 'https://avatars.githubusercontent.com/u/314135?v=4',
+ });
+ render();
+ const img = screen.getByTestId('server-icon-img');
+ expect(img.tagName).toBe('IMG');
+ expect(img).toHaveAttribute(
+ 'src',
+ 'https://avatars.githubusercontent.com/u/314135?v=4'
+ );
+ });
+
+ it('should show fallback when image fails to load', () => {
+ const server = makeServer({
+ icon: 'https://example.com/broken-icon.png',
+ });
+ render();
+ const img = screen.getByTestId('server-icon-img');
+ fireEvent.error(img);
+ expect(screen.getByTestId('server-icon-fallback')).toHaveTextContent('📦');
+ });
+ });
+
+ describe('server info', () => {
+ it('should render server name', () => {
+ const server = makeServer({ name: 'GitHub MCP Server' });
+ render();
+ expect(screen.getByText('GitHub MCP Server')).toBeInTheDocument();
+ });
+
+ it('should render description', () => {
+ const server = makeServer({ description: 'Manage GitHub repos' });
+ render();
+ expect(screen.getByText('Manage GitHub repos')).toBeInTheDocument();
+ });
+
+ it('should render categories', () => {
+ const server = makeServer({
+ categories: ['cloud', 'developer-tools', 'productivity'],
+ });
+ render();
+ expect(screen.getByText('cloud')).toBeInTheDocument();
+ expect(screen.getByText('developer-tools')).toBeInTheDocument();
+ expect(screen.getByText('productivity')).toBeInTheDocument();
+ });
+
+ it('should truncate categories beyond 3', () => {
+ const server = makeServer({
+ categories: ['cloud', 'developer-tools', 'productivity', 'extra'],
+ });
+ render();
+ expect(screen.getByText('+1')).toBeInTheDocument();
+ });
+ });
+
+ describe('actions', () => {
+ it('should render Install button for non-installed server', () => {
+ const server = makeServer({ is_installed: false });
+ render();
+ expect(screen.getByText('Install')).toBeInTheDocument();
+ });
+
+ it('should render Uninstall button for installed server', () => {
+ const server = makeServer({ is_installed: true });
+ render();
+ expect(screen.getByText('Uninstall')).toBeInTheDocument();
+ });
+
+ it('should call onViewDetails when card is clicked', () => {
+ const server = makeServer();
+ render();
+ fireEvent.click(screen.getByTestId(`server-card-${server.id}`));
+ expect(defaultProps.onViewDetails).toHaveBeenCalledWith(server);
+ });
+ });
+});
diff --git a/tests/ts/components/ServerDetailModal.test.tsx b/tests/ts/components/ServerDetailModal.test.tsx
new file mode 100644
index 00000000..e1585c3a
--- /dev/null
+++ b/tests/ts/components/ServerDetailModal.test.tsx
@@ -0,0 +1,109 @@
+import { describe, it, expect, vi } from 'vitest';
+import { render, screen } from '@testing-library/react';
+import { ServerDetailModal } from '../../../apps/desktop/src/features/registry/ServerDetailModal';
+import type { ServerViewModel } from '../../../apps/desktop/src/types/registry';
+
+function makeServer(overrides: Partial = {}): ServerViewModel {
+ return {
+ id: 'com.test-server',
+ name: 'Test Server',
+ description: 'A test MCP server',
+ alias: 'test',
+ icon: null,
+ auth: { type: 'none' },
+ transport: {
+ type: 'http',
+ url: 'https://example.com/mcp',
+ headers: {},
+ metadata: { inputs: [] },
+ },
+ categories: ['developer-tools'],
+ publisher: null,
+ source: { type: 'Registry', url: 'https://registry.mcpmux.com', name: 'McpMux Registry' },
+ is_installed: false,
+ enabled: false,
+ oauth_connected: false,
+ input_values: {},
+ connection_status: 'disconnected',
+ missing_required_inputs: false,
+ last_error: null,
+ ...overrides,
+ };
+}
+
+describe('ServerDetailModal', () => {
+ const defaultProps = {
+ onClose: vi.fn(),
+ onInstall: vi.fn(),
+ onUninstall: vi.fn(),
+ };
+
+ it('should render server name', () => {
+ const server = makeServer({ name: 'Cloudflare Workers' });
+ render();
+ expect(screen.getByText('Cloudflare Workers')).toBeInTheDocument();
+ });
+
+ it('should render fallback icon when icon is null', () => {
+ const server = makeServer({ icon: null });
+ render();
+ expect(screen.getByTestId('server-icon-fallback')).toHaveTextContent('📦');
+ });
+
+ it('should render emoji icon as text', () => {
+ const server = makeServer({ icon: '🔐' });
+ render();
+ expect(screen.getByTestId('server-icon-emoji')).toHaveTextContent('🔐');
+ });
+
+ it('should render URL icon as img element', () => {
+ const server = makeServer({
+ icon: 'https://avatars.githubusercontent.com/u/314135?v=4',
+ });
+ render();
+ const img = screen.getByTestId('server-icon-img');
+ expect(img.tagName).toBe('IMG');
+ expect(img).toHaveAttribute(
+ 'src',
+ 'https://avatars.githubusercontent.com/u/314135?v=4'
+ );
+ });
+
+ it('should render description', () => {
+ const server = makeServer({ description: 'Manages KV and R2 buckets' });
+ render();
+ expect(screen.getByText('Manages KV and R2 buckets')).toBeInTheDocument();
+ });
+
+ it('should render categories', () => {
+ const server = makeServer({ categories: ['cloud', 'developer-tools'] });
+ render();
+ expect(screen.getByText('cloud')).toBeInTheDocument();
+ expect(screen.getByText('developer-tools')).toBeInTheDocument();
+ });
+
+ it('should render hosting type for remote server', () => {
+ const server = makeServer({
+ transport: {
+ type: 'http',
+ url: 'https://example.com/mcp',
+ headers: {},
+ metadata: { inputs: [] },
+ },
+ });
+ render();
+ expect(screen.getByText(/Remote Server/)).toBeInTheDocument();
+ });
+
+ it('should render Install button for non-installed server', () => {
+ const server = makeServer({ is_installed: false });
+ render();
+ expect(screen.getByText('Install')).toBeInTheDocument();
+ });
+
+ it('should render Uninstall button for installed server', () => {
+ const server = makeServer({ is_installed: true });
+ render();
+ expect(screen.getByText('Uninstall')).toBeInTheDocument();
+ });
+});
diff --git a/tests/ts/components/ServerIcon.test.tsx b/tests/ts/components/ServerIcon.test.tsx
new file mode 100644
index 00000000..8a8c92e1
--- /dev/null
+++ b/tests/ts/components/ServerIcon.test.tsx
@@ -0,0 +1,121 @@
+import { describe, it, expect } from 'vitest';
+import { render, screen, fireEvent } from '@testing-library/react';
+import { ServerIcon } from '../../../apps/desktop/src/components/ServerIcon';
+
+describe('ServerIcon', () => {
+ describe('fallback rendering', () => {
+ it('should render default fallback when icon is null', () => {
+ render();
+ const fallback = screen.getByTestId('server-icon-fallback');
+ expect(fallback).toBeInTheDocument();
+ expect(fallback).toHaveTextContent('📦');
+ });
+
+ it('should render default fallback when icon is undefined', () => {
+ render();
+ const fallback = screen.getByTestId('server-icon-fallback');
+ expect(fallback).toBeInTheDocument();
+ expect(fallback).toHaveTextContent('📦');
+ });
+
+ it('should render default fallback when icon is empty string', () => {
+ render();
+ const fallback = screen.getByTestId('server-icon-fallback');
+ expect(fallback).toBeInTheDocument();
+ expect(fallback).toHaveTextContent('📦');
+ });
+
+ it('should render custom fallback when specified', () => {
+ render();
+ const fallback = screen.getByTestId('server-icon-fallback');
+ expect(fallback).toHaveTextContent('🔌');
+ });
+ });
+
+ describe('emoji rendering', () => {
+ it('should render emoji icon as text', () => {
+ render();
+ const emoji = screen.getByTestId('server-icon-emoji');
+ expect(emoji).toBeInTheDocument();
+ expect(emoji).toHaveTextContent('🔐');
+ });
+
+ it('should render non-URL text as emoji', () => {
+ render();
+ const emoji = screen.getByTestId('server-icon-emoji');
+ expect(emoji).toBeInTheDocument();
+ expect(emoji).toHaveTextContent('test-icon');
+ });
+ });
+
+ describe('URL icon rendering', () => {
+ it('should render HTTP URL as img element', () => {
+ render(
+
+ );
+ const img = screen.getByTestId('server-icon-img');
+ expect(img).toBeInTheDocument();
+ expect(img.tagName).toBe('IMG');
+ expect(img).toHaveAttribute('src', 'http://example.com/icon.png');
+ });
+
+ it('should render HTTPS URL as img element', () => {
+ render(
+
+ );
+ const img = screen.getByTestId('server-icon-img');
+ expect(img).toBeInTheDocument();
+ expect(img.tagName).toBe('IMG');
+ expect(img).toHaveAttribute(
+ 'src',
+ 'https://avatars.githubusercontent.com/u/314135?v=4'
+ );
+ });
+
+ it('should apply custom className to img element', () => {
+ render(
+
+ );
+ const img = screen.getByTestId('server-icon-img');
+ expect(img).toHaveClass('w-12', 'h-12', 'rounded-lg');
+ });
+
+ it('should apply default className to img element', () => {
+ render(
+
+ );
+ const img = screen.getByTestId('server-icon-img');
+ expect(img).toHaveClass('w-9', 'h-9', 'object-contain');
+ });
+
+ it('should show fallback when image fails to load', () => {
+ render(
+
+ );
+ const img = screen.getByTestId('server-icon-img');
+ expect(img).toBeInTheDocument();
+
+ // Simulate image load error
+ fireEvent.error(img);
+
+ // Should now show fallback
+ const fallback = screen.getByTestId('server-icon-fallback');
+ expect(fallback).toBeInTheDocument();
+ expect(fallback).toHaveTextContent('📦');
+ });
+
+ it('should show custom fallback when image fails to load', () => {
+ render(
+
+ );
+ const img = screen.getByTestId('server-icon-img');
+ fireEvent.error(img);
+
+ const fallback = screen.getByTestId('server-icon-fallback');
+ expect(fallback).toHaveTextContent('⚠️');
+ });
+ });
+});