master/develop 共用 release 轨(App_Store 打包构建号自增),feature 分支走
feature 轨(构建号永不自增,始终使用手填值),两条轨道的 App_Ver 各自独立。
- versions 改为 tracks 结构,旧的单轨/单值配置在加载时自动迁移进 release 轨
- 新增 config["branch_track"] 记录分支归属,缺省按分支名推断,可在分支管理页改
- 新增 PUT /api/config/branches/track;分支名含 / 时走请求体而非路径参数
- 修复 DELETE /api/config/branches/{name} 无法删除含 / 的分支(405)
- 版本号接口不再对普通用户开放,打包设置页整页改为管理员可见
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
252 lines
8.4 KiB
JavaScript
252 lines
8.4 KiB
JavaScript
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' }])
|
|
})
|
|
})
|