Initial commit: iOS Build Server

- FastAPI backend with build queue, WebSocket logs, task management
- Vue 3 frontend with build/config/history views
- Xcode project build automation with IPA export
- Fix: initialize build_dir before try block to ensure cleanup on early failure
This commit is contained in:
shen
2026-06-06 17:42:27 +08:00
commit 6f4f625c56
51 changed files with 9968 additions and 0 deletions
+164
View File
@@ -0,0 +1,164 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { mount, flushPromises } from '@vue/test-utils'
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': { apps: {}, schemes: {}, branches: ['main'] },
}
return Promise.resolve({
json: () => Promise.resolve(responses[url] || {}),
})
}
describe('ConfigView.vue', () => {
beforeEach(() => {
vi.clearAllMocks()
localStorageMock.clear()
localStorageMock.getItem.mockReturnValue(null)
fetch.mockImplementation(mockFetch)
})
it('未登录时显示权限提示', async () => {
const wrapper = mount(ConfigView)
await flushPromises()
expect(wrapper.text()).toContain('需要管理员权限')
})
it('已登录时显示管理页面', async () => {
localStorageMock.getItem.mockReturnValue('true')
const wrapper = mount(ConfigView)
await flushPromises()
expect(wrapper.text()).toContain('配置管理')
})
it('显示侧边栏菜单', async () => {
localStorageMock.getItem.mockReturnValue('true')
const wrapper = mount(ConfigView)
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('默认显示服务器环境 tab', async () => {
localStorageMock.getItem.mockReturnValue('true')
const wrapper = mount(ConfigView)
await flushPromises()
expect(wrapper.text()).toContain('服务器环境配置')
})
it('切换到分支管理 tab', async () => {
localStorageMock.getItem.mockReturnValue('true')
const wrapper = mount(ConfigView)
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)
})
localStorageMock.getItem.mockReturnValue('true')
const wrapper = mount(ConfigView)
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)
})
localStorageMock.getItem.mockReturnValue('true')
const wrapper = mount(ConfigView)
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()
localStorageMock.getItem.mockReturnValue('true')
const wrapper = mount(ConfigView)
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('设置已保存')
})
})