You've already forked iFlow-Settings-Editor-GUI
新增 单元测试框架和测试用例
This commit is contained in:
80
src/components/Footer.test.js
Normal file
80
src/components/Footer.test.js
Normal file
@@ -0,0 +1,80 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import Footer from './Footer.vue';
|
||||
|
||||
describe('Footer.vue', () => {
|
||||
it('renders correctly with default props', () => {
|
||||
const wrapper = mount(Footer, {
|
||||
global: {
|
||||
mocks: {
|
||||
$t: (key) => key,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.exists()).toBe(true);
|
||||
expect(wrapper.find('.footer').exists()).toBe(true);
|
||||
expect(wrapper.find('.footer-status').exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('displays current profile correctly', () => {
|
||||
const wrapper = mount(Footer, {
|
||||
props: {
|
||||
currentProfile: 'dev',
|
||||
},
|
||||
global: {
|
||||
mocks: {
|
||||
$t: (key) => key,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const statusText = wrapper.find('.footer-status').text();
|
||||
expect(statusText).toContain('dev');
|
||||
});
|
||||
|
||||
it('displays default profile when no prop provided', () => {
|
||||
const wrapper = mount(Footer, {
|
||||
global: {
|
||||
mocks: {
|
||||
$t: (key) => key,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const statusText = wrapper.find('.footer-status').text();
|
||||
expect(statusText).toContain('default');
|
||||
});
|
||||
|
||||
it('displays status dot', () => {
|
||||
const wrapper = mount(Footer, {
|
||||
props: {
|
||||
currentProfile: 'production',
|
||||
},
|
||||
global: {
|
||||
mocks: {
|
||||
$t: (key) => key,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.find('.footer-status-dot').exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('applies translation correctly', () => {
|
||||
const wrapper = mount(Footer, {
|
||||
props: {
|
||||
currentProfile: 'test-profile',
|
||||
},
|
||||
global: {
|
||||
mocks: {
|
||||
$t: (key) => `translated-${key}`,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const statusText = wrapper.find('.footer-status').text();
|
||||
expect(statusText).toContain('translated-api.currentConfig');
|
||||
expect(statusText).toContain('test-profile');
|
||||
});
|
||||
});
|
||||
164
src/components/SideBar.test.js
Normal file
164
src/components/SideBar.test.js
Normal file
@@ -0,0 +1,164 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import SideBar from './SideBar.vue';
|
||||
|
||||
describe('SideBar.vue', () => {
|
||||
it('renders correctly with default props', () => {
|
||||
const wrapper = mount(SideBar, {
|
||||
global: {
|
||||
mocks: {
|
||||
$t: (key) => key,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.exists()).toBe(true);
|
||||
expect(wrapper.find('.sidebar').exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('has three nav items', () => {
|
||||
const wrapper = mount(SideBar, {
|
||||
global: {
|
||||
mocks: {
|
||||
$t: (key) => key,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const navItems = wrapper.findAll('.nav-item');
|
||||
expect(navItems.length).toBe(3);
|
||||
});
|
||||
|
||||
it('has two sections', () => {
|
||||
const wrapper = mount(SideBar, {
|
||||
global: {
|
||||
mocks: {
|
||||
$t: (key) => key,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const sections = wrapper.findAll('.sidebar-section');
|
||||
expect(sections.length).toBe(2);
|
||||
});
|
||||
|
||||
it('highlights active section correctly', () => {
|
||||
const wrapper = mount(SideBar, {
|
||||
props: {
|
||||
currentSection: 'api',
|
||||
},
|
||||
global: {
|
||||
mocks: {
|
||||
$t: (key) => key,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const navItems = wrapper.findAll('.nav-item');
|
||||
expect(navItems[0].classes('active')).toBe(false);
|
||||
expect(navItems[1].classes('active')).toBe(true);
|
||||
expect(navItems[2].classes('active')).toBe(false);
|
||||
});
|
||||
|
||||
it('emits navigate event when nav item is clicked', async () => {
|
||||
const wrapper = mount(SideBar, {
|
||||
props: {
|
||||
currentSection: 'general',
|
||||
},
|
||||
global: {
|
||||
mocks: {
|
||||
$t: (key) => key,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const navItems = wrapper.findAll('.nav-item');
|
||||
await navItems[1].trigger('click');
|
||||
|
||||
expect(wrapper.emitted('navigate')).toBeTruthy();
|
||||
expect(wrapper.emitted('navigate')[0][0]).toBe('api');
|
||||
});
|
||||
|
||||
it('displays server count badge correctly', () => {
|
||||
const wrapper = mount(SideBar, {
|
||||
props: {
|
||||
currentSection: 'general',
|
||||
serverCount: 5
|
||||
},
|
||||
global: {
|
||||
mocks: {
|
||||
$t: (key) => key,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const badges = wrapper.findAll('.nav-item-badge');
|
||||
expect(badges.length).toBe(1);
|
||||
expect(badges[0].text()).toBe('5');
|
||||
});
|
||||
|
||||
it('displays zero server count', () => {
|
||||
const wrapper = mount(SideBar, {
|
||||
props: {
|
||||
currentSection: 'general',
|
||||
serverCount: 0
|
||||
},
|
||||
global: {
|
||||
mocks: {
|
||||
$t: (key) => key,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const badges = wrapper.findAll('.nav-item-badge');
|
||||
expect(badges.length).toBe(1);
|
||||
expect(badges[0].text()).toBe('0');
|
||||
});
|
||||
|
||||
it('applies translation to section titles', () => {
|
||||
const wrapper = mount(SideBar, {
|
||||
global: {
|
||||
mocks: {
|
||||
$t: (key) => `translated-${key}`,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const sectionTitles = wrapper.findAll('.sidebar-title');
|
||||
expect(sectionTitles[0].text()).toBe('translated-sidebar.general');
|
||||
expect(sectionTitles[1].text()).toBe('translated-sidebar.advanced');
|
||||
});
|
||||
|
||||
it('applies translation to nav item texts', () => {
|
||||
const wrapper = mount(SideBar, {
|
||||
global: {
|
||||
mocks: {
|
||||
$t: (key) => `translated-${key}`,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const navItems = wrapper.findAll('.nav-item-text');
|
||||
expect(navItems[0].text()).toBe('translated-sidebar.basicSettings');
|
||||
expect(navItems[1].text()).toBe('translated-sidebar.apiConfig');
|
||||
expect(navItems[2].text()).toBe('translated-sidebar.mcpServers');
|
||||
});
|
||||
|
||||
it('handles null currentSection', () => {
|
||||
const wrapper = mount(SideBar, {
|
||||
props: {
|
||||
currentSection: null,
|
||||
},
|
||||
global: {
|
||||
mocks: {
|
||||
$t: (key) => key,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const navItems = wrapper.findAll('.nav-item');
|
||||
expect(navItems[0].classes('active')).toBe(false);
|
||||
expect(navItems[1].classes('active')).toBe(false);
|
||||
expect(navItems[2].classes('active')).toBe(false);
|
||||
});
|
||||
});
|
||||
131
src/components/TitleBar.test.js
Normal file
131
src/components/TitleBar.test.js
Normal file
@@ -0,0 +1,131 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import TitleBar from './TitleBar.vue';
|
||||
|
||||
describe('TitleBar.vue', () => {
|
||||
beforeEach(() => {
|
||||
// Mock window.electronAPI
|
||||
global.window.electronAPI = {
|
||||
minimize: vi.fn(),
|
||||
maximize: vi.fn(),
|
||||
close: vi.fn()
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders correctly', () => {
|
||||
const wrapper = mount(TitleBar, {
|
||||
global: {
|
||||
mocks: {
|
||||
$t: (key) => key,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.exists()).toBe(true);
|
||||
expect(wrapper.find('.titlebar').exists()).toBe(true);
|
||||
expect(wrapper.find('.titlebar-title').exists()).toBe(true);
|
||||
expect(wrapper.find('.titlebar-controls').exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('displays app title', () => {
|
||||
const wrapper = mount(TitleBar, {
|
||||
global: {
|
||||
mocks: {
|
||||
$t: (key) => key,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.find('.titlebar-title').text()).toBe('app.title');
|
||||
});
|
||||
|
||||
it('has three window control buttons', () => {
|
||||
const wrapper = mount(TitleBar, {
|
||||
global: {
|
||||
mocks: {
|
||||
$t: (key) => key,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const buttons = wrapper.findAll('.titlebar-btn');
|
||||
expect(buttons.length).toBe(3);
|
||||
});
|
||||
|
||||
it('calls minimize when minimize button is clicked', async () => {
|
||||
const wrapper = mount(TitleBar, {
|
||||
global: {
|
||||
mocks: {
|
||||
$t: (key) => key,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const minimizeButton = wrapper.findAll('.titlebar-btn')[0];
|
||||
await minimizeButton.trigger('click');
|
||||
|
||||
expect(window.electronAPI.minimize).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('calls maximize when maximize button is clicked', async () => {
|
||||
const wrapper = mount(TitleBar, {
|
||||
global: {
|
||||
mocks: {
|
||||
$t: (key) => key,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const maximizeButton = wrapper.findAll('.titlebar-btn')[1];
|
||||
await maximizeButton.trigger('click');
|
||||
|
||||
expect(window.electronAPI.maximize).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('calls close when close button is clicked', async () => {
|
||||
const wrapper = mount(TitleBar, {
|
||||
global: {
|
||||
mocks: {
|
||||
$t: (key) => key,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const closeButton = wrapper.findAll('.titlebar-btn')[2];
|
||||
await closeButton.trigger('click');
|
||||
|
||||
expect(window.electronAPI.close).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('has close button with close class', () => {
|
||||
const wrapper = mount(TitleBar, {
|
||||
global: {
|
||||
mocks: {
|
||||
$t: (key) => key,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const closeButton = wrapper.findAll('.titlebar-btn')[2];
|
||||
expect(closeButton.classes()).toContain('close');
|
||||
});
|
||||
|
||||
it('applies translation to button tooltips', () => {
|
||||
const wrapper = mount(TitleBar, {
|
||||
global: {
|
||||
mocks: {
|
||||
$t: (key) => `translated-${key}`,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const buttons = wrapper.findAll('.titlebar-btn');
|
||||
expect(buttons[0].attributes('title')).toBe('translated-window.minimize');
|
||||
expect(buttons[1].attributes('title')).toBe('translated-window.maximize');
|
||||
expect(buttons[2].attributes('title')).toBe('translated-window.close');
|
||||
});
|
||||
});
|
||||
@@ -280,6 +280,11 @@ body {
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.form-required {
|
||||
color: var(--danger);
|
||||
margin-left: 3px;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
padding: 10px 14px;
|
||||
@@ -434,6 +439,12 @@ body {
|
||||
transform: translateY(0) scale(0.98);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-secondary);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-primary);
|
||||
@@ -447,7 +458,7 @@ body {
|
||||
.btn-danger {
|
||||
background: var(--danger);
|
||||
color: white;
|
||||
box-shadow: 0 2px 4px rgba(239, 68, 68, 0.3);
|
||||
border: 1px solid var(--danger);
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
@@ -470,6 +481,32 @@ body {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
// Side panel close button (used by ServerPanel and ApiProfileDialog)
|
||||
.side-panel-close {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-tertiary);
|
||||
cursor: pointer;
|
||||
border-radius: var(--radius);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.side-panel-close:hover {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
.side-panel-close svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
stroke: currentColor;
|
||||
stroke-width: 1.5;
|
||||
fill: none;
|
||||
}
|
||||
|
||||
// Empty state
|
||||
.empty-state {
|
||||
display: flex;
|
||||
@@ -518,6 +555,10 @@ body {
|
||||
animation: fadeIn 0.15s ease;
|
||||
}
|
||||
|
||||
.dialog-overlay-top {
|
||||
z-index: 1400;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
background: var(--bg-secondary);
|
||||
border-radius: var(--radius-lg);
|
||||
@@ -542,6 +583,12 @@ body {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.dialog-body {
|
||||
padding: 20px 24px;
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.dialog-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
344
src/views/ApiConfig.test.js
Normal file
344
src/views/ApiConfig.test.js
Normal file
@@ -0,0 +1,344 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import ApiConfig from './ApiConfig.vue';
|
||||
|
||||
describe('ApiConfig.vue', () => {
|
||||
const mockSettings = {
|
||||
apiProfiles: {
|
||||
'default': {
|
||||
baseUrl: 'https://api.default.com',
|
||||
selectedAuthType: 'openai-compatible',
|
||||
apiKey: '',
|
||||
modelName: '',
|
||||
searchApiKey: '',
|
||||
cna: ''
|
||||
},
|
||||
'dev': {
|
||||
baseUrl: 'https://api.dev.com',
|
||||
selectedAuthType: 'openai-compatible',
|
||||
apiKey: 'dev-key',
|
||||
modelName: 'gpt-4',
|
||||
searchApiKey: '',
|
||||
cna: ''
|
||||
},
|
||||
'prod': {
|
||||
baseUrl: 'https://api.prod.com',
|
||||
selectedAuthType: 'openai-compatible',
|
||||
apiKey: 'prod-key',
|
||||
modelName: 'gpt-4',
|
||||
searchApiKey: '',
|
||||
cna: ''
|
||||
}
|
||||
},
|
||||
currentApiProfile: 'default'
|
||||
};
|
||||
|
||||
const mockProfiles = [
|
||||
{ name: 'default' },
|
||||
{ name: 'dev' },
|
||||
{ name: 'prod' }
|
||||
];
|
||||
|
||||
it('renders correctly with props', () => {
|
||||
const wrapper = mount(ApiConfig, {
|
||||
props: {
|
||||
profiles: mockProfiles,
|
||||
currentProfile: 'default',
|
||||
settings: mockSettings,
|
||||
},
|
||||
global: {
|
||||
mocks: {
|
||||
$t: (key) => key,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.exists()).toBe(true);
|
||||
expect(wrapper.find('.content-title').exists()).toBe(true);
|
||||
expect(wrapper.find('.card').exists()).toBe(true);
|
||||
expect(wrapper.find('.profile-list').exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('displays all profiles', () => {
|
||||
const wrapper = mount(ApiConfig, {
|
||||
props: {
|
||||
profiles: mockProfiles,
|
||||
currentProfile: 'default',
|
||||
settings: mockSettings,
|
||||
},
|
||||
global: {
|
||||
mocks: {
|
||||
$t: (key) => key,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const profileItems = wrapper.findAll('.profile-item');
|
||||
expect(profileItems.length).toBe(3);
|
||||
});
|
||||
|
||||
it('highlights current profile', () => {
|
||||
const wrapper = mount(ApiConfig, {
|
||||
props: {
|
||||
profiles: mockProfiles,
|
||||
currentProfile: 'dev',
|
||||
settings: mockSettings,
|
||||
},
|
||||
global: {
|
||||
mocks: {
|
||||
$t: (key) => key,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const profileItems = wrapper.findAll('.profile-item');
|
||||
expect(profileItems[0].classes('active')).toBe(false);
|
||||
expect(profileItems[1].classes('active')).toBe(true);
|
||||
expect(profileItems[2].classes('active')).toBe(false);
|
||||
});
|
||||
|
||||
it('shows status badge only for current profile', () => {
|
||||
const wrapper = mount(ApiConfig, {
|
||||
props: {
|
||||
profiles: mockProfiles,
|
||||
currentProfile: 'default',
|
||||
settings: mockSettings,
|
||||
},
|
||||
global: {
|
||||
mocks: {
|
||||
$t: (key) => key,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const statusBadges = wrapper.findAll('.status-badge');
|
||||
expect(statusBadges.length).toBe(1);
|
||||
expect(wrapper.findAll('.profile-item')[0].find('.status-badge').exists()).toBe(true);
|
||||
expect(wrapper.findAll('.profile-item')[1].find('.status-badge').exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('emits create-profile event when create button is clicked', async () => {
|
||||
const wrapper = mount(ApiConfig, {
|
||||
props: {
|
||||
profiles: mockProfiles,
|
||||
currentProfile: 'default',
|
||||
settings: mockSettings,
|
||||
},
|
||||
global: {
|
||||
mocks: {
|
||||
$t: (key) => key,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await wrapper.find('.btn-primary').trigger('click');
|
||||
expect(wrapper.emitted('create-profile')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('emits select-profile event when profile is clicked', async () => {
|
||||
const wrapper = mount(ApiConfig, {
|
||||
props: {
|
||||
profiles: mockProfiles,
|
||||
currentProfile: 'default',
|
||||
settings: mockSettings,
|
||||
},
|
||||
global: {
|
||||
mocks: {
|
||||
$t: (key) => key,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const profileItems = wrapper.findAll('.profile-item');
|
||||
await profileItems[1].trigger('click');
|
||||
|
||||
expect(wrapper.emitted('select-profile')).toBeTruthy();
|
||||
expect(wrapper.emitted('select-profile')[0][0]).toBe('dev');
|
||||
});
|
||||
|
||||
it('emits edit-profile event when edit button is clicked', async () => {
|
||||
const wrapper = mount(ApiConfig, {
|
||||
props: {
|
||||
profiles: mockProfiles,
|
||||
currentProfile: 'default',
|
||||
settings: mockSettings,
|
||||
},
|
||||
global: {
|
||||
mocks: {
|
||||
$t: (key) => key,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const editButtons = wrapper.findAll('.action-btn');
|
||||
await editButtons[0].trigger('click');
|
||||
|
||||
expect(wrapper.emitted('edit-profile')).toBeTruthy();
|
||||
expect(wrapper.emitted('edit-profile')[0][0]).toBe('default');
|
||||
});
|
||||
|
||||
it('emits duplicate-profile event when duplicate button is clicked', async () => {
|
||||
const wrapper = mount(ApiConfig, {
|
||||
props: {
|
||||
profiles: mockProfiles,
|
||||
currentProfile: 'default',
|
||||
settings: mockSettings,
|
||||
},
|
||||
global: {
|
||||
mocks: {
|
||||
$t: (key) => key,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const duplicateButtons = wrapper.findAll('.action-btn');
|
||||
await duplicateButtons[1].trigger('click');
|
||||
|
||||
expect(wrapper.emitted('duplicate-profile')).toBeTruthy();
|
||||
expect(wrapper.emitted('duplicate-profile')[0][0]).toBe('default');
|
||||
});
|
||||
|
||||
it('shows delete button only for non-default profiles', () => {
|
||||
const wrapper = mount(ApiConfig, {
|
||||
props: {
|
||||
profiles: mockProfiles,
|
||||
currentProfile: 'default',
|
||||
settings: mockSettings,
|
||||
},
|
||||
global: {
|
||||
mocks: {
|
||||
$t: (key) => key,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const profileItems = wrapper.findAll('.profile-item');
|
||||
const deleteButtons = wrapper.findAll('.action-btn-danger');
|
||||
|
||||
expect(deleteButtons.length).toBe(2);
|
||||
expect(profileItems[0].find('.action-btn-danger').exists()).toBe(false);
|
||||
expect(profileItems[1].find('.action-btn-danger').exists()).toBe(true);
|
||||
expect(profileItems[2].find('.action-btn-danger').exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('emits delete-profile event when delete button is clicked', async () => {
|
||||
const wrapper = mount(ApiConfig, {
|
||||
props: {
|
||||
profiles: mockProfiles,
|
||||
currentProfile: 'default',
|
||||
settings: mockSettings,
|
||||
},
|
||||
global: {
|
||||
mocks: {
|
||||
$t: (key) => key,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const deleteButtons = wrapper.findAll('.action-btn-danger');
|
||||
await deleteButtons[0].trigger('click');
|
||||
|
||||
expect(wrapper.emitted('delete-profile')).toBeTruthy();
|
||||
expect(wrapper.emitted('delete-profile')[0][0]).toBe('dev');
|
||||
});
|
||||
|
||||
it('displays correct profile names', () => {
|
||||
const wrapper = mount(ApiConfig, {
|
||||
props: {
|
||||
profiles: mockProfiles,
|
||||
currentProfile: 'default',
|
||||
settings: mockSettings,
|
||||
},
|
||||
global: {
|
||||
mocks: {
|
||||
$t: (key) => key,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const profileNames = wrapper.findAll('.profile-name');
|
||||
expect(profileNames[0].text()).toBe('default');
|
||||
expect(profileNames[1].text()).toBe('dev');
|
||||
expect(profileNames[2].text()).toBe('prod');
|
||||
});
|
||||
|
||||
it('displays correct profile URLs', () => {
|
||||
const wrapper = mount(ApiConfig, {
|
||||
props: {
|
||||
profiles: mockProfiles,
|
||||
currentProfile: 'default',
|
||||
settings: mockSettings,
|
||||
},
|
||||
global: {
|
||||
mocks: {
|
||||
$t: (key) => key,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const profileUrls = wrapper.findAll('.profile-url');
|
||||
expect(profileUrls[0].text()).toBe('https://api.default.com');
|
||||
expect(profileUrls[1].text()).toBe('https://api.dev.com');
|
||||
expect(profileUrls[2].text()).toBe('https://api.prod.com');
|
||||
});
|
||||
|
||||
it('displays correct profile initials', () => {
|
||||
const wrapper = mount(ApiConfig, {
|
||||
props: {
|
||||
profiles: mockProfiles,
|
||||
currentProfile: 'default',
|
||||
settings: mockSettings,
|
||||
},
|
||||
global: {
|
||||
mocks: {
|
||||
$t: (key) => key,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const iconTexts = wrapper.findAll('.profile-icon-text');
|
||||
expect(iconTexts[0].text()).toBe('D');
|
||||
expect(iconTexts[1].text()).toBe('D');
|
||||
expect(iconTexts[2].text()).toBe('P');
|
||||
});
|
||||
|
||||
it('handles empty profiles array', () => {
|
||||
const wrapper = mount(ApiConfig, {
|
||||
props: {
|
||||
profiles: [],
|
||||
currentProfile: 'default',
|
||||
settings: mockSettings,
|
||||
},
|
||||
global: {
|
||||
mocks: {
|
||||
$t: (key) => key,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const profileItems = wrapper.findAll('.profile-item');
|
||||
expect(profileItems.length).toBe(0);
|
||||
});
|
||||
|
||||
it('handles missing apiProfiles in settings', () => {
|
||||
const settingsWithoutProfiles = { currentApiProfile: 'default' };
|
||||
|
||||
const wrapper = mount(ApiConfig, {
|
||||
props: {
|
||||
profiles: mockProfiles,
|
||||
currentProfile: 'default',
|
||||
settings: settingsWithoutProfiles,
|
||||
},
|
||||
global: {
|
||||
mocks: {
|
||||
$t: (key) => key,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const profileUrls = wrapper.findAll('.profile-url');
|
||||
expect(profileUrls[0].text()).toBe('');
|
||||
expect(profileUrls[1].text()).toBe('');
|
||||
expect(profileUrls[2].text()).toBe('');
|
||||
});
|
||||
});
|
||||
157
src/views/GeneralSettings.test.js
Normal file
157
src/views/GeneralSettings.test.js
Normal file
@@ -0,0 +1,157 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { nextTick } from 'vue';
|
||||
import GeneralSettings from './GeneralSettings.vue';
|
||||
|
||||
describe('GeneralSettings.vue', () => {
|
||||
const mockSettings = {
|
||||
language: 'zh-CN',
|
||||
theme: 'Xcode',
|
||||
bootAnimationShown: true,
|
||||
checkpointing: { enabled: true },
|
||||
};
|
||||
|
||||
it('renders correctly with props', () => {
|
||||
const wrapper = mount(GeneralSettings, {
|
||||
props: {
|
||||
settings: mockSettings,
|
||||
},
|
||||
global: {
|
||||
mocks: {
|
||||
$t: (key) => key,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.exists()).toBe(true);
|
||||
expect(wrapper.find('.content-title').exists()).toBe(true);
|
||||
expect(wrapper.findAll('.card').length).toBe(2);
|
||||
});
|
||||
|
||||
it('displays language options correctly', () => {
|
||||
const wrapper = mount(GeneralSettings, {
|
||||
props: {
|
||||
settings: mockSettings,
|
||||
},
|
||||
global: {
|
||||
mocks: {
|
||||
$t: (key) => key,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const languageOptions = wrapper.findAll('.form-select')[0].findAll('option');
|
||||
expect(languageOptions.length).toBe(3);
|
||||
expect(languageOptions[0].attributes('value')).toBe('zh-CN');
|
||||
expect(languageOptions[1].attributes('value')).toBe('en-US');
|
||||
expect(languageOptions[2].attributes('value')).toBe('ja-JP');
|
||||
});
|
||||
|
||||
it('displays theme options correctly', () => {
|
||||
const wrapper = mount(GeneralSettings, {
|
||||
props: {
|
||||
settings: mockSettings,
|
||||
},
|
||||
global: {
|
||||
mocks: {
|
||||
$t: (key) => key,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const themeOptions = wrapper.findAll('.form-select')[1].findAll('option');
|
||||
expect(themeOptions.length).toBe(4);
|
||||
expect(themeOptions[0].attributes('value')).toBe('Xcode');
|
||||
expect(themeOptions[1].attributes('value')).toBe('Dark');
|
||||
expect(themeOptions[2].attributes('value')).toBe('Light');
|
||||
expect(themeOptions[3].attributes('value')).toBe('Solarized Dark');
|
||||
});
|
||||
|
||||
it('reflects current settings in form controls', async () => {
|
||||
const wrapper = mount(GeneralSettings, {
|
||||
props: {
|
||||
settings: mockSettings,
|
||||
},
|
||||
global: {
|
||||
mocks: {
|
||||
$t: (key) => key,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await nextTick();
|
||||
const selectElements = wrapper.findAll('.form-select');
|
||||
expect(selectElements[0].element.value).toBe('zh-CN');
|
||||
expect(selectElements[1].element.value).toBe('Xcode');
|
||||
expect(selectElements[2].element.value).toBe('true');
|
||||
expect(selectElements[3].element.value).toBe('true');
|
||||
});
|
||||
|
||||
it('applies translation correctly', () => {
|
||||
const wrapper = mount(GeneralSettings, {
|
||||
props: {
|
||||
settings: mockSettings,
|
||||
},
|
||||
global: {
|
||||
mocks: {
|
||||
$t: (key) => `translated-${key}`,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.find('.content-title').text()).toBe('translated-general.title');
|
||||
expect(wrapper.find('.content-desc').text()).toBe('translated-general.description');
|
||||
});
|
||||
|
||||
it('has two cards for settings sections', () => {
|
||||
const wrapper = mount(GeneralSettings, {
|
||||
props: {
|
||||
settings: mockSettings,
|
||||
},
|
||||
global: {
|
||||
mocks: {
|
||||
$t: (key) => key,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const cards = wrapper.findAll('.card');
|
||||
expect(cards.length).toBe(2);
|
||||
});
|
||||
|
||||
it('displays card titles with icons', () => {
|
||||
const wrapper = mount(GeneralSettings, {
|
||||
props: {
|
||||
settings: mockSettings,
|
||||
},
|
||||
global: {
|
||||
mocks: {
|
||||
$t: (key) => key,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const cardTitles = wrapper.findAll('.card-title');
|
||||
expect(cardTitles.length).toBe(2);
|
||||
expect(cardTitles[0].text()).toContain('general.languageInterface');
|
||||
expect(cardTitles[1].text()).toContain('general.otherSettings');
|
||||
});
|
||||
|
||||
it('shows all form controls with proper structure', () => {
|
||||
const wrapper = mount(GeneralSettings, {
|
||||
props: {
|
||||
settings: mockSettings,
|
||||
},
|
||||
global: {
|
||||
mocks: {
|
||||
$t: (key) => key,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.findAll('.form-row').length).toBe(2);
|
||||
expect(wrapper.findAll('.form-group').length).toBe(4);
|
||||
expect(wrapper.findAll('.form-label').length).toBe(4);
|
||||
expect(wrapper.findAll('.form-select').length).toBe(4);
|
||||
});
|
||||
});
|
||||
256
src/views/McpServers.test.js
Normal file
256
src/views/McpServers.test.js
Normal file
@@ -0,0 +1,256 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import McpServers from './McpServers.vue';
|
||||
|
||||
describe('McpServers.vue', () => {
|
||||
const mockServers = {
|
||||
'server1': {
|
||||
description: '第一个服务器',
|
||||
command: 'node server.js',
|
||||
args: ['--port', '3000'],
|
||||
env: {}
|
||||
},
|
||||
'server2': {
|
||||
description: '第二个服务器',
|
||||
command: 'python server.py',
|
||||
args: [],
|
||||
env: { 'PYTHONPATH': '/path/to/python' }
|
||||
},
|
||||
'server3': {
|
||||
command: 'java -jar server.jar',
|
||||
args: [],
|
||||
env: {}
|
||||
}
|
||||
};
|
||||
|
||||
it('renders correctly with props', () => {
|
||||
const wrapper = mount(McpServers, {
|
||||
props: {
|
||||
servers: mockServers,
|
||||
selectedServer: 'server1',
|
||||
serverCount: 3
|
||||
},
|
||||
global: {
|
||||
mocks: {
|
||||
$t: (key) => key,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.exists()).toBe(true);
|
||||
expect(wrapper.find('.content-title').exists()).toBe(true);
|
||||
expect(wrapper.find('.server-list').exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('displays all servers', () => {
|
||||
const wrapper = mount(McpServers, {
|
||||
props: {
|
||||
servers: mockServers,
|
||||
selectedServer: 'server1',
|
||||
serverCount: 3
|
||||
},
|
||||
global: {
|
||||
mocks: {
|
||||
$t: (key) => key,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const serverItems = wrapper.findAll('.server-item');
|
||||
expect(serverItems.length).toBe(3);
|
||||
});
|
||||
|
||||
it('highlights selected server', () => {
|
||||
const wrapper = mount(McpServers, {
|
||||
props: {
|
||||
servers: mockServers,
|
||||
selectedServer: 'server2',
|
||||
serverCount: 3
|
||||
},
|
||||
global: {
|
||||
mocks: {
|
||||
$t: (key) => key,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const serverItems = wrapper.findAll('.server-item');
|
||||
expect(serverItems[0].classes('selected')).toBe(false);
|
||||
expect(serverItems[1].classes('selected')).toBe(true);
|
||||
expect(serverItems[2].classes('selected')).toBe(false);
|
||||
});
|
||||
|
||||
it('shows empty state when no servers', () => {
|
||||
const wrapper = mount(McpServers, {
|
||||
props: {
|
||||
servers: {},
|
||||
selectedServer: null,
|
||||
serverCount: 0
|
||||
},
|
||||
global: {
|
||||
mocks: {
|
||||
$t: (key) => key,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.find('.empty-state').exists()).toBe(true);
|
||||
expect(wrapper.find('.empty-state-title').exists()).toBe(true);
|
||||
expect(wrapper.find('.empty-state-desc').exists()).toBe(true);
|
||||
expect(wrapper.findAll('.server-item').length).toBe(0);
|
||||
});
|
||||
|
||||
it('emits add-server event when add button is clicked', async () => {
|
||||
const wrapper = mount(McpServers, {
|
||||
props: {
|
||||
servers: mockServers,
|
||||
selectedServer: 'server1',
|
||||
serverCount: 3
|
||||
},
|
||||
global: {
|
||||
mocks: {
|
||||
$t: (key) => key,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await wrapper.find('.btn-primary').trigger('click');
|
||||
expect(wrapper.emitted('add-server')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('emits select-server event when server is clicked', async () => {
|
||||
const wrapper = mount(McpServers, {
|
||||
props: {
|
||||
servers: mockServers,
|
||||
selectedServer: 'server1',
|
||||
serverCount: 3
|
||||
},
|
||||
global: {
|
||||
mocks: {
|
||||
$t: (key) => key,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const serverItems = wrapper.findAll('.server-item');
|
||||
await serverItems[1].trigger('click');
|
||||
|
||||
expect(wrapper.emitted('select-server')).toBeTruthy();
|
||||
expect(wrapper.emitted('select-server')[0][0]).toBe('server2');
|
||||
});
|
||||
|
||||
it('displays correct server names', () => {
|
||||
const wrapper = mount(McpServers, {
|
||||
props: {
|
||||
servers: mockServers,
|
||||
selectedServer: 'server1',
|
||||
serverCount: 3
|
||||
},
|
||||
global: {
|
||||
mocks: {
|
||||
$t: (key) => key,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const serverNames = wrapper.findAll('.server-name');
|
||||
expect(serverNames[0].text()).toBe('server1');
|
||||
expect(serverNames[1].text()).toBe('server2');
|
||||
expect(serverNames[2].text()).toBe('server3');
|
||||
});
|
||||
|
||||
it('displays correct server descriptions', () => {
|
||||
const wrapper = mount(McpServers, {
|
||||
props: {
|
||||
servers: mockServers,
|
||||
selectedServer: 'server1',
|
||||
serverCount: 3
|
||||
},
|
||||
global: {
|
||||
mocks: {
|
||||
$t: (key) => key,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const serverDescs = wrapper.findAll('.server-desc');
|
||||
expect(serverDescs[0].text()).toBe('第一个服务器');
|
||||
expect(serverDescs[1].text()).toBe('第二个服务器');
|
||||
expect(serverDescs[2].text()).toBe('mcp.noDescription');
|
||||
});
|
||||
|
||||
it('displays status indicators for all servers', () => {
|
||||
const wrapper = mount(McpServers, {
|
||||
props: {
|
||||
servers: mockServers,
|
||||
selectedServer: 'server1',
|
||||
serverCount: 3
|
||||
},
|
||||
global: {
|
||||
mocks: {
|
||||
$t: (key) => key,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const statusIndicators = wrapper.findAll('.server-status');
|
||||
expect(statusIndicators.length).toBe(3);
|
||||
});
|
||||
|
||||
it('handles null selectedServer prop', () => {
|
||||
const wrapper = mount(McpServers, {
|
||||
props: {
|
||||
servers: mockServers,
|
||||
selectedServer: null,
|
||||
serverCount: 3
|
||||
},
|
||||
global: {
|
||||
mocks: {
|
||||
$t: (key) => key,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const serverItems = wrapper.findAll('.server-item');
|
||||
expect(serverItems.length).toBe(3);
|
||||
expect(serverItems[0].classes('selected')).toBe(false);
|
||||
expect(serverItems[1].classes('selected')).toBe(false);
|
||||
expect(serverItems[2].classes('selected')).toBe(false);
|
||||
});
|
||||
|
||||
it('handles zero serverCount with empty servers object', () => {
|
||||
const wrapper = mount(McpServers, {
|
||||
props: {
|
||||
servers: {},
|
||||
selectedServer: null,
|
||||
serverCount: 0
|
||||
},
|
||||
global: {
|
||||
mocks: {
|
||||
$t: (key) => key,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.find('.empty-state').exists()).toBe(true);
|
||||
expect(wrapper.findAll('.server-item').length).toBe(0);
|
||||
});
|
||||
|
||||
it('displays empty state title correctly', () => {
|
||||
const wrapper = mount(McpServers, {
|
||||
props: {
|
||||
servers: {},
|
||||
selectedServer: null,
|
||||
serverCount: 0
|
||||
},
|
||||
global: {
|
||||
mocks: {
|
||||
$t: (key) => key,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.find('.empty-state-title').text()).toBe('mcp.noServers');
|
||||
expect(wrapper.find('.empty-state-desc').text()).toBe('mcp.addFirstServer');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user