// FE-COMP-INTEGRATIONS-001 to FE-COMP-INTEGRATIONS-018
import { render, screen, waitFor } from '../../../tests/helpers/render';
import userEvent from '@testing-library/user-event';
import { http, HttpResponse } from 'msw';
import { server } from '../../../tests/helpers/msw/server';
import { useAuthStore } from '../../store/authStore';
import { useAddonStore } from '../../store/addonStore';
import { resetAllStores, seedStore } from '../../../tests/helpers/store';
import { buildUser } from '../../../tests/helpers/factories';
import IntegrationsTab from './IntegrationsTab';
function enableMcp() {
seedStore(useAddonStore, {
addons: [{ id: 'mcp', name: 'MCP', type: 'integration', icon: '', enabled: true }],
loaded: true,
loadAddons: vi.fn(),
});
}
const clipboardWriteText = vi.fn().mockResolvedValue(undefined);
beforeAll(() => {
Object.defineProperty(navigator, 'clipboard', {
value: { writeText: clipboardWriteText },
configurable: true,
writable: true,
});
});
beforeEach(() => {
clipboardWriteText.mockClear();
resetAllStores();
vi.clearAllMocks();
seedStore(useAuthStore, { user: buildUser(), isAuthenticated: true });
seedStore(useAddonStore, {
addons: [],
loaded: true,
loadAddons: vi.fn(),
});
server.use(
http.get('/api/auth/mcp-tokens', () => HttpResponse.json({ tokens: [] })),
http.get('/api/addons', () => HttpResponse.json({ addons: [] })),
);
});
describe('IntegrationsTab', () => {
it('FE-COMP-INTEGRATIONS-001: renders without crashing (MCP disabled)', () => {
render();
expect(document.body).toBeInTheDocument();
});
it('FE-COMP-INTEGRATIONS-002: MCP section is hidden when mcp addon is disabled', () => {
render();
expect(screen.queryByText('MCP Configuration')).toBeNull();
});
it('FE-COMP-INTEGRATIONS-003: MCP section is visible when mcp addon is enabled', async () => {
enableMcp();
render();
await screen.findByText('MCP Configuration');
});
it('FE-COMP-INTEGRATIONS-004: MCP endpoint URL is displayed', async () => {
enableMcp();
render();
await screen.findByText('MCP Configuration');
const codeEl = document.querySelector('code');
expect(codeEl).not.toBeNull();
expect(codeEl!.textContent).toContain('/mcp');
});
it('FE-COMP-INTEGRATIONS-005: JSON config block is rendered', async () => {
enableMcp();
render();
await screen.findByText('MCP Configuration');
const preEl = document.querySelector('pre');
expect(preEl).not.toBeNull();
expect(preEl!.textContent).toContain('mcpServers');
});
it('FE-COMP-INTEGRATIONS-006: "no tokens" message shown when token list is empty', async () => {
enableMcp();
render();
await screen.findByText('No tokens yet. Create one to connect MCP clients.');
});
it('FE-COMP-INTEGRATIONS-007: token list renders when tokens exist', async () => {
server.use(
http.get('/api/auth/mcp-tokens', () =>
HttpResponse.json({
tokens: [
{ id: 1, name: 'My Token', token_prefix: 'tk_aaa', created_at: '2025-01-01T00:00:00.000Z', last_used_at: null },
{ id: 2, name: 'Other Token', token_prefix: 'tk_bbb', created_at: '2025-01-01T00:00:00.000Z', last_used_at: null },
],
}),
),
);
enableMcp();
render();
await screen.findByText('My Token');
await screen.findByText('Other Token');
});
it('FE-COMP-INTEGRATIONS-008: clicking "Create New Token" button opens the modal', async () => {
const user = userEvent.setup();
enableMcp();
render();
await screen.findByText('MCP Configuration');
const createBtn = screen.getByRole('button', { name: /Create New Token/i });
await user.click(createBtn);
await screen.findByText('Create API Token');
});
it('FE-COMP-INTEGRATIONS-009: Create button in modal is disabled when name is empty', async () => {
const user = userEvent.setup();
enableMcp();
render();
await screen.findByText('MCP Configuration');
await user.click(screen.getByRole('button', { name: /Create New Token/i }));
await screen.findByText('Create API Token');
const modalCreateBtn = screen.getByRole('button', { name: /^Create Token$/i });
expect(modalCreateBtn).toBeDisabled();
});
it('FE-COMP-INTEGRATIONS-010: Create button in modal becomes enabled when name is typed', async () => {
const user = userEvent.setup();
enableMcp();
render();
await screen.findByText('MCP Configuration');
await user.click(screen.getByRole('button', { name: /Create New Token/i }));
await screen.findByText('Create API Token');
const input = screen.getByPlaceholderText(/Claude Desktop/i);
await user.type(input, 'My API token');
const modalCreateBtn = screen.getByRole('button', { name: /^Create Token$/i });
expect(modalCreateBtn).not.toBeDisabled();
});
it('FE-COMP-INTEGRATIONS-011: creating a token calls the API and shows the raw token', async () => {
server.use(
http.post('/api/auth/mcp-tokens', () =>
HttpResponse.json({
token: {
id: 1,
name: 'test',
token_prefix: 'tk_abc',
created_at: '2025-01-01T00:00:00.000Z',
raw_token: 'tk_abc...full_secret_token',
},
}),
),
);
const user = userEvent.setup();
enableMcp();
render();
await screen.findByText('MCP Configuration');
await user.click(screen.getByRole('button', { name: /Create New Token/i }));
await screen.findByText('Create API Token');
const input = screen.getByPlaceholderText(/Claude Desktop/i);
await user.type(input, 'test');
await user.click(screen.getByRole('button', { name: /^Create Token$/i }));
// Raw token should be displayed
await screen.findByText(/tk_abc\.\.\.full_secret_token/);
// Warning about one-time display
expect(screen.getByText(/only be shown once/i)).toBeInTheDocument();
});
it('FE-COMP-INTEGRATIONS-012: "Done" button closes the token-created modal', async () => {
server.use(
http.post('/api/auth/mcp-tokens', () =>
HttpResponse.json({
token: {
id: 1,
name: 'test',
token_prefix: 'tk_abc',
created_at: '2025-01-01T00:00:00.000Z',
raw_token: 'tk_abc...full_secret_token',
},
}),
),
);
const user = userEvent.setup();
enableMcp();
render();
await screen.findByText('MCP Configuration');
await user.click(screen.getByRole('button', { name: /Create New Token/i }));
await screen.findByText('Create API Token');
await user.type(screen.getByPlaceholderText(/Claude Desktop/i), 'test');
await user.click(screen.getByRole('button', { name: /^Create Token$/i }));
await screen.findByText('Token Created');
await user.click(screen.getByRole('button', { name: /^Done$/i }));
await waitFor(() => {
expect(screen.queryByText('Token Created')).toBeNull();
});
});
it('FE-COMP-INTEGRATIONS-013: clicking the delete button next to a token opens the confirm modal', async () => {
server.use(
http.get('/api/auth/mcp-tokens', () =>
HttpResponse.json({
tokens: [
{ id: 1, name: 'Delete Me', token_prefix: 'tk_del', created_at: '2025-01-01T00:00:00.000Z', last_used_at: null },
],
}),
),
);
const user = userEvent.setup();
enableMcp();
render();
await screen.findByText('Delete Me');
await user.click(screen.getByTitle('Delete Token'));
await screen.findByText('This token will stop working immediately. Any MCP client using it will lose access.');
expect(screen.getByRole('button', { name: /^Cancel$/i })).toBeInTheDocument();
});
it('FE-COMP-INTEGRATIONS-014: confirming deletion calls DELETE API and removes token from list', async () => {
let deleteCalled = false;
server.use(
http.get('/api/auth/mcp-tokens', () =>
HttpResponse.json({
tokens: [
{ id: 1, name: 'Delete Me', token_prefix: 'tk_del', created_at: '2025-01-01T00:00:00.000Z', last_used_at: null },
],
}),
),
http.delete('/api/auth/mcp-tokens/1', () => {
deleteCalled = true;
return HttpResponse.json({ success: true });
}),
);
const user = userEvent.setup();
enableMcp();
render();
await screen.findByText('Delete Me');
await user.click(screen.getByTitle('Delete Token'));
// There are two "Delete Token" buttons: the trash icon (title) and the confirm button in modal
const deleteButtons = await screen.findAllByRole('button', { name: /^Delete Token$/i });
// Click the one in the modal (last one, or the standalone one without title attribute)
const confirmBtn = deleteButtons.find(btn => !btn.title);
await user.click(confirmBtn ?? deleteButtons[deleteButtons.length - 1]);
expect(deleteCalled).toBe(true);
await waitFor(() => {
expect(screen.queryByText('Delete Me')).toBeNull();
});
});
it('FE-COMP-INTEGRATIONS-015: copying endpoint URL calls clipboard.writeText', async () => {
const user = userEvent.setup();
enableMcp();
render();
await screen.findByText('MCP Configuration');
// Spy after userEvent.setup() may have replaced navigator.clipboard
const writeSpy = vi.spyOn(navigator.clipboard, 'writeText').mockResolvedValue(undefined);
const copyBtns = screen.getAllByTitle('Copy');
await user.click(copyBtns[0]);
expect(writeSpy).toHaveBeenCalledWith(expect.stringContaining('/mcp'));
});
it('FE-COMP-INTEGRATIONS-016: copy button shows checkmark icon after copy', async () => {
const user = userEvent.setup();
enableMcp();
render();
await screen.findByText('MCP Configuration');
vi.spyOn(navigator.clipboard, 'writeText').mockResolvedValue(undefined);
const copyBtns = screen.getAllByTitle('Copy');
await user.click(copyBtns[0]);
await waitFor(() => {
// After copy, icon changes to Check (green). The button should contain an svg with text-green-500
const btn = copyBtns[0];
const svg = btn.querySelector('svg');
expect(svg).toHaveClass('text-green-500');
});
});
it('FE-COMP-INTEGRATIONS-017: cancel button in delete confirm modal closes it without API call', async () => {
let deleteCalled = false;
server.use(
http.get('/api/auth/mcp-tokens', () =>
HttpResponse.json({
tokens: [
{ id: 1, name: 'Cancel Token', token_prefix: 'tk_can', created_at: '2025-01-01T00:00:00.000Z', last_used_at: null },
],
}),
),
http.delete('/api/auth/mcp-tokens/1', () => {
deleteCalled = true;
return HttpResponse.json({ success: true });
}),
);
const user = userEvent.setup();
enableMcp();
render();
await screen.findByText('Cancel Token');
await user.click(screen.getByTitle('Delete Token'));
await screen.findByRole('button', { name: /^Cancel$/i });
await user.click(screen.getByRole('button', { name: /^Cancel$/i }));
await waitFor(() => {
expect(screen.queryByText('This token will stop working immediately. Any MCP client using it will lose access.')).toBeNull();
});
expect(deleteCalled).toBe(false);
});
it('FE-COMP-INTEGRATIONS-018: pressing Enter in the token name input triggers creation', async () => {
let postCalled = false;
server.use(
http.post('/api/auth/mcp-tokens', () => {
postCalled = true;
return HttpResponse.json({
token: {
id: 1,
name: 'enter-test',
token_prefix: 'tk_ent',
created_at: '2025-01-01T00:00:00.000Z',
raw_token: 'tk_ent...full',
},
});
}),
);
const user = userEvent.setup();
enableMcp();
render();
await screen.findByText('MCP Configuration');
await user.click(screen.getByRole('button', { name: /Create New Token/i }));
await screen.findByText('Create API Token');
const input = screen.getByPlaceholderText(/Claude Desktop/i);
await user.type(input, 'enter-test');
await user.keyboard('{Enter}');
await waitFor(() => {
expect(postCalled).toBe(true);
});
});
});