Files
n8n/packages/modules/mcp/frontend/src/components/MCPWorkflowsSelect.test.ts
T
24dda0d435 refactor(editor): Extract MCP access into a frontend module package (no-changelog) (#39493)
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
2026-10-06 14:31:21 +00:00

385 lines
10 KiB
TypeScript

import { nextTick } from 'vue';
import { createTestingPinia } from '@pinia/testing';
import { waitFor } from '@testing-library/vue';
import userEvent from '@testing-library/user-event';
import { createComponentRenderer, mockedStore, type MockedStore } from '@n8n/frontend-test-utils';
import MCPWorkflowsSelect from './MCPWorkflowsSelect.vue';
import { useMCPStore } from '../mcp.store';
import { createWorkflow } from '../mcp.test.utils';
import type { McpWorkflow } from '../mcp.types';
vi.mock('vue-router', async (importOriginal) => ({
...(await importOriginal()),
useRouter: () => ({
resolve: vi.fn(({ name, params }) => {
if (name === 'NodeViewExisting') {
return { fullPath: `/workflows/${params.workflowId}` };
}
if (name === 'ProjectsWorkflows') {
return { fullPath: `/projects/${params.projectId}` };
}
return { fullPath: '/' };
}),
}),
}));
let pinia: ReturnType<typeof createTestingPinia>;
let mcpStore: MockedStore<typeof useMCPStore>;
const createComponent = createComponentRenderer(MCPWorkflowsSelect);
describe('MCPWorkflowsSelect', () => {
beforeEach(() => {
pinia = createTestingPinia();
mcpStore = mockedStore(useMCPStore);
mcpStore.getMcpEligibleWorkflows.mockResolvedValue({
count: 0,
data: [],
});
});
afterEach(() => {
vi.clearAllMocks();
});
describe('Initial rendering', () => {
it('should render the select component', async () => {
const { getByTestId } = createComponent({ pinia });
await nextTick();
expect(getByTestId('mcp-workflows-select')).toBeInTheDocument();
});
it('should render with placeholder prop', async () => {
const { getByTestId } = createComponent({
pinia,
props: {
placeholder: 'Search workflows...',
},
});
await nextTick();
const select = getByTestId('mcp-workflows-select');
// In multiple mode the placeholder lives on the main input, not the inline filter input
const inputs = Array.from(select.querySelectorAll('input'));
expect(
inputs.some((input) => input.getAttribute('placeholder') === 'Search workflows...'),
).toBe(true);
});
it('should render as disabled when disabled prop is true', async () => {
const { container } = createComponent({
pinia,
props: {
disabled: true,
},
});
await nextTick();
// The disabled class is applied to the inner el-select element
const elSelect = container.querySelector('.el-select');
expect(elSelect).toHaveClass('el-select--disabled');
});
});
describe('Fetching workflows on mount', () => {
it('should fetch workflows on mount', async () => {
createComponent({ pinia });
await nextTick();
expect(mcpStore.getMcpEligibleWorkflows).toHaveBeenCalledWith({
take: 10,
query: undefined,
});
});
it('should handle async workflow fetching', async () => {
// Create a promise that doesn't resolve immediately
let resolvePromise: (value: { count: number; data: McpWorkflow[] }) => void;
mcpStore.getMcpEligibleWorkflows.mockImplementation(
async () =>
await new Promise((resolve) => {
resolvePromise = resolve;
}),
);
const { getByTestId } = createComponent({ pinia });
await nextTick();
// The component should be rendered while loading
expect(getByTestId('mcp-workflows-select')).toBeInTheDocument();
// Resolve the promise with workflows
const workflows = [createWorkflow({ id: 'wf-1', name: 'Test Workflow' })];
resolvePromise!({ count: 1, data: workflows });
await nextTick();
// Verify the API was called
expect(mcpStore.getMcpEligibleWorkflows).toHaveBeenCalled();
});
});
describe('Workflow options rendering', () => {
it('should render workflow options after fetching', async () => {
const workflows = [
createWorkflow({ id: 'wf-1', name: 'Workflow One' }),
createWorkflow({ id: 'wf-2', name: 'Workflow Two' }),
];
mcpStore.getMcpEligibleWorkflows.mockResolvedValue({
count: 2,
data: workflows,
});
const { getByTestId } = createComponent({ pinia });
await nextTick();
// Wait for loading to complete
await waitFor(() => {
expect(mcpStore.getMcpEligibleWorkflows).toHaveBeenCalled();
});
const select = getByTestId('mcp-workflows-select');
await userEvent.click(select);
await waitFor(() => {
const options = document.querySelectorAll('.el-select-dropdown__item');
expect(options.length).toBe(2);
});
});
it('should show empty state when no workflows are returned', async () => {
mcpStore.getMcpEligibleWorkflows.mockResolvedValue({
count: 0,
data: [],
});
const { getByTestId } = createComponent({ pinia });
await nextTick();
// Wait for the fetch to complete
await waitFor(() => {
expect(mcpStore.getMcpEligibleWorkflows).toHaveBeenCalled();
});
const select = getByTestId('mcp-workflows-select');
await userEvent.click(select);
await waitFor(() => {
const emptyText = document.querySelector('.el-select-dropdown__empty');
expect(emptyText).toBeInTheDocument();
});
});
});
describe('Remote search', () => {
it('should search workflows when user types in input', async () => {
mcpStore.getMcpEligibleWorkflows.mockResolvedValue({
count: 0,
data: [],
});
const { getByTestId } = createComponent({ pinia });
await nextTick();
// Wait for initial fetch
await waitFor(() => {
expect(mcpStore.getMcpEligibleWorkflows).toHaveBeenCalled();
});
// Clear mocks to track new calls
mcpStore.getMcpEligibleWorkflows.mockClear();
mcpStore.getMcpEligibleWorkflows.mockResolvedValue({
count: 1,
data: [createWorkflow({ id: 'wf-1', name: 'Searched Workflow' })],
});
const select = getByTestId('mcp-workflows-select');
const input = select.querySelector('input');
if (input) {
await userEvent.type(input, 'test');
}
await waitFor(() => {
expect(mcpStore.getMcpEligibleWorkflows).toHaveBeenCalledWith({
take: 10,
query: 'test',
});
});
});
});
describe('v-model binding', () => {
it('should collect selected options into the modelValue array', async () => {
const workflows = [
createWorkflow({ id: 'wf-1', name: 'Selectable Workflow' }),
createWorkflow({ id: 'wf-2', name: 'Another Workflow' }),
];
mcpStore.getMcpEligibleWorkflows.mockResolvedValue({
count: 2,
data: workflows,
});
const { getByTestId, emitted } = createComponent({ pinia });
await nextTick();
// Wait for fetch to complete
await waitFor(() => {
expect(mcpStore.getMcpEligibleWorkflows).toHaveBeenCalled();
});
const select = getByTestId('mcp-workflows-select');
await userEvent.click(select);
await waitFor(() => {
const options = document.querySelectorAll('.el-select-dropdown__item');
expect(options.length).toBe(2);
});
const options = document.querySelectorAll('.el-select-dropdown__item');
await userEvent.click(options[0]);
await waitFor(() => {
expect(emitted('update:modelValue')).toBeTruthy();
expect(emitted('update:modelValue')[0]).toEqual([['wf-1']]);
});
await userEvent.click(options[1]);
await waitFor(() => {
expect(emitted('update:modelValue')[1]).toEqual([['wf-1', 'wf-2']]);
});
});
});
describe('WorkflowLocation rendering in options', () => {
it('should render workflow location with project name', async () => {
const workflows = [
createWorkflow({
id: 'wf-1',
name: 'My Workflow',
homeProject: {
id: 'project-1',
type: 'team',
name: 'Team Project',
icon: null,
createdAt: '2025-01-01',
updatedAt: '2025-01-01',
},
}),
];
mcpStore.getMcpEligibleWorkflows.mockResolvedValue({
count: 1,
data: workflows,
});
const { getByTestId } = createComponent({ pinia });
await nextTick();
// Wait for fetch to complete
await waitFor(() => {
expect(mcpStore.getMcpEligibleWorkflows).toHaveBeenCalled();
});
const select = getByTestId('mcp-workflows-select');
await userEvent.click(select);
await waitFor(() => {
const projectName = document.querySelector(
'[data-test-id="workflow-location-project-name"]',
);
expect(projectName).toBeInTheDocument();
expect(projectName).toHaveTextContent('Team Project');
});
});
it('should render workflow location with folder when parentFolder is provided', async () => {
const workflows = [
createWorkflow({
id: 'wf-1',
name: 'My Workflow',
homeProject: {
id: 'project-1',
type: 'team',
name: 'Team Project',
icon: null,
createdAt: '2025-01-01',
updatedAt: '2025-01-01',
},
parentFolder: {
id: 'folder-1',
name: 'My Folder',
parentFolderId: null,
},
}),
];
mcpStore.getMcpEligibleWorkflows.mockResolvedValue({
count: 1,
data: workflows,
});
const { getByTestId } = createComponent({ pinia });
await nextTick();
// Wait for fetch to complete
await waitFor(() => {
expect(mcpStore.getMcpEligibleWorkflows).toHaveBeenCalled();
});
const select = getByTestId('mcp-workflows-select');
await userEvent.click(select);
await waitFor(() => {
const folderName = document.querySelector('[data-test-id="workflow-location-folder-name"]');
expect(folderName).toBeInTheDocument();
expect(folderName).toHaveTextContent('My Folder');
});
});
it('should render Personal for personal project type', async () => {
const workflows = [
createWorkflow({
id: 'wf-1',
name: 'My Workflow',
homeProject: {
id: 'personal-project',
type: 'personal',
name: 'User<[email protected]>',
icon: null,
createdAt: '2025-01-01',
updatedAt: '2025-01-01',
},
}),
];
mcpStore.getMcpEligibleWorkflows.mockResolvedValue({
count: 1,
data: workflows,
});
const { getByTestId } = createComponent({ pinia });
await nextTick();
// Wait for fetch to complete
await waitFor(() => {
expect(mcpStore.getMcpEligibleWorkflows).toHaveBeenCalled();
});
const select = getByTestId('mcp-workflows-select');
await userEvent.click(select);
await waitFor(() => {
const projectName = document.querySelector(
'[data-test-id="workflow-location-project-name"]',
);
expect(projectName).toBeInTheDocument();
expect(projectName).toHaveTextContent('Personal');
});
});
});
});