import { describe, it, expect, vi, beforeEach } from 'vitest' import { mount, flushPromises } from '@vue/test-utils' import { ref } from 'vue' import ConfigView from '../views/ConfigView.vue' global.fetch = vi.fn() // Mock localStorage const localStorageMock = (() => { let store = {} return { getItem: vi.fn(k => store[k] || null), setItem: vi.fn((k, v) => { store[k] = v }), removeItem: vi.fn(k => { delete store[k] }), clear: vi.fn(() => { store = {} }), } })() Object.defineProperty(global, 'localStorage', { value: localStorageMock }) function mockFetch(url) { const responses = { '/api/config/apps': { '1': { name: '测试App', server: '测试环境', AppGuid: 'guid' } }, '/api/config/schemes': { '1': { name: 'testScheme', ossFloder: 'test' } }, '/api/config/servers': { '测试环境': { api: 'https://test.com', assDom: '', universalLink: '' } }, '/api/config/branches': ['main', 'dev'], '/api/config/build': { max_concurrent_builds: 2, build_dir_retention_hours: 24, build_base_dir: '/tmp' }, '/api/config/versions': { app_ver: '2.195.0', build_ver: '2.195.0.0', tracks: { release: { app_ver: '2.195.0', build_ver: '2.195.0.0', build_map: {} }, feature: { app_ver: '2.100.0', build_ver: '2.100.0.0', build_map: {} }, }, branch_track: { main: 'release', dev: 'release' }, }, '/api/config': { apps: {}, schemes: {}, branches: ['main'] }, } return Promise.resolve({ json: () => Promise.resolve(responses[url] || {}), }) } const mountConfigView = ({ loggedIn = true, admin = true } = {}) => mount(ConfigView, { global: { provide: { showLogin: ref(false), getToken: () => '', isLoggedIn: ref(loggedIn), isAdmin: ref(admin), }, }, }) describe('ConfigView.vue', () => { beforeEach(() => { vi.clearAllMocks() localStorageMock.clear() localStorageMock.getItem.mockReturnValue(null) fetch.mockImplementation(mockFetch) }) it('未登录时显示权限提示', async () => { const wrapper = mountConfigView({ loggedIn: false }) await flushPromises() expect(wrapper.text()).toContain('请先登录') }) it('已登录时显示管理页面', async () => { const wrapper = mountConfigView() await flushPromises() expect(wrapper.text()).toContain('配置管理') }) it('显示侧边栏菜单', async () => { const wrapper = mountConfigView() await flushPromises() const menuItems = wrapper.findAll('.sidebar-menu li') const texts = menuItems.map(li => li.text()) expect(texts).toContain('服务器环境') expect(texts).toContain('Apps 配置') expect(texts).toContain('Schemes 配置') expect(texts).toContain('分支管理') expect(texts).toContain('打包设置') }) it('默认显示 Apps 配置 tab', async () => { const wrapper = mountConfigView() await flushPromises() expect(wrapper.text()).toContain('Apps 配置') }) it('切换到分支管理 tab', async () => { const wrapper = mountConfigView() await flushPromises() const branchesMenu = wrapper.findAll('.sidebar-menu li').find(li => li.text() === '分支管理') await branchesMenu.trigger('click') await wrapper.vm.$nextTick() expect(wrapper.text()).toContain('main') expect(wrapper.text()).toContain('dev') }) it('添加分支', async () => { fetch.mockImplementation((url, opts) => { if (url === '/api/config/branches' && opts?.method === 'POST') { return Promise.resolve({ ok: true, json: () => Promise.resolve({ message: 'ok' }) }) } return mockFetch(url) }) const wrapper = mountConfigView() await flushPromises() const branchesMenu = wrapper.findAll('.sidebar-menu li').find(li => li.text() === '分支管理') await branchesMenu.trigger('click') await wrapper.vm.$nextTick() const input = wrapper.find('input[placeholder="输入分支名称"]') await input.setValue('new-branch') const addBtn = wrapper.findAll('.btn-primary').find(b => b.text() === '添加分支') await addBtn.trigger('click') await flushPromises() expect(fetch).toHaveBeenCalledWith('/api/config/branches', expect.objectContaining({ method: 'POST', })) }) it('删除分支', async () => { window.confirm = vi.fn(() => true) fetch.mockImplementation((url, opts) => { if (url.includes('/api/config/branches/') && opts?.method === 'DELETE') { return Promise.resolve({ ok: true, json: () => Promise.resolve({ message: 'ok' }) }) } return mockFetch(url) }) const wrapper = mountConfigView() await flushPromises() const branchesMenu = wrapper.findAll('.sidebar-menu li').find(li => li.text() === '分支管理') await branchesMenu.trigger('click') await wrapper.vm.$nextTick() const deleteBtns = wrapper.findAll('.action-btn.delete') if (deleteBtns.length > 0) { await deleteBtns[0].trigger('click') await flushPromises() expect(window.confirm).toHaveBeenCalled() } }) it('保存打包设置', async () => { fetch.mockImplementation((url, opts) => { if (url === '/api/config/build' && opts?.method === 'PUT') { return Promise.resolve({ ok: true, json: () => Promise.resolve({ message: 'ok' }) }) } return mockFetch(url) }) window.alert = vi.fn() const wrapper = mountConfigView() await flushPromises() const buildMenu = wrapper.findAll('.sidebar-menu li').find(li => li.text() === '打包设置') await buildMenu.trigger('click') await wrapper.vm.$nextTick() const saveBtn = wrapper.findAll('.btn-primary').find(b => b.text() === '保存设置') await saveBtn.trigger('click') await flushPromises() expect(window.alert).toHaveBeenCalledWith('设置已保存') }) it('普通账号只能管理 Apps,看不到打包设置与版本号', async () => { const wrapper = mountConfigView({ admin: false }) await flushPromises() const menuTexts = wrapper.findAll('.sidebar-menu li').map(li => li.text()) expect(menuTexts).toEqual(['Apps 配置']) expect(wrapper.text()).not.toContain('上架版本号') expect(wrapper.text()).not.toContain('自测版本号') expect(wrapper.text()).not.toContain('打包参数') // 版本号接口为管理员专属,普通账号不再请求 expect(fetch).not.toHaveBeenCalledWith('/api/config/versions', expect.anything()) expect(fetch.mock.calls.map(c => c[0])).toEqual(['/api/config/apps']) }) it('两条版本轨道各自独立展示与保存', async () => { const calls = [] fetch.mockImplementation((url, opts) => { if (url === '/api/config/versions' && opts?.method === 'PUT') { calls.push(JSON.parse(opts.body)) return Promise.resolve({ ok: true, json: () => Promise.resolve({ app_ver: '2.101.0', build_ver: '2.101.0.0' }), }) } return mockFetch(url) }) window.alert = vi.fn() const wrapper = mountConfigView() await flushPromises() const buildMenu = wrapper.findAll('.sidebar-menu li').find(li => li.text() === '打包设置') await buildMenu.trigger('click') await wrapper.vm.$nextTick() const inputs = wrapper.findAll('input[type="text"]') expect(inputs[0].element.value).toBe('2.195.0') // release App_Ver expect(inputs[2].element.value).toBe('2.100.0') // feature App_Ver await inputs[2].setValue('2.101.0') const saveBtn = wrapper.findAll('.btn-primary').find(b => b.text() === '保存自测版本号') await saveBtn.trigger('click') await flushPromises() expect(calls).toEqual([{ track: 'feature', app_ver: '2.101.0', build_ver: '2.101.0.0' }]) }) it('分支管理可切换版本轨道', async () => { const calls = [] fetch.mockImplementation((url, opts) => { if (url === '/api/config/branches/track' && opts?.method === 'PUT') { calls.push(JSON.parse(opts.body)) return Promise.resolve({ ok: true, json: () => Promise.resolve({ message: 'ok' }) }) } return mockFetch(url) }) const wrapper = mountConfigView() await flushPromises() const branchesMenu = wrapper.findAll('.sidebar-menu li').find(li => li.text() === '分支管理') await branchesMenu.trigger('click') await wrapper.vm.$nextTick() const select = wrapper.find('.config-table select') expect(select.element.value).toBe('release') await select.setValue('feature') await flushPromises() expect(calls).toEqual([{ branch: 'main', track: 'feature' }]) }) })