feat: 收口组件样式并调整移动端侧栏

This commit is contained in:
GJ
2026-09-04 17:19:34 +08:00
parent d252d5513f
commit e6e0f08072
68 changed files with 2717 additions and 2775 deletions
@@ -0,0 +1,166 @@
import { flushPromises, mount, type VueWrapper } from '@vue/test-utils'
import { afterEach, describe, expect, it, vi } from 'vitest'
import WorkflowDiagnosticsDialog from '@/features/workflows/WorkflowDiagnosticsDialog.vue'
import { loadWorkflowDiagnostics, type WorkflowTimelineGroup } from '@/features/workflows/diagnostics'
import type { Checkpoint } from '@/features/workflows/types'
let wrapper: VueWrapper | undefined
afterEach(() => {
wrapper?.unmount()
wrapper = undefined
document.body.innerHTML = ''
vi.unstubAllGlobals()
})
/** 后端将 checkpoint 固定标为 completed,测试确保界面不把它当执行成功。 */
const groups: WorkflowTimelineGroup[] = [
{
phase: '其它',
nodeCount: 22,
durationMs: 22000,
durationText: '22s',
nodes: Array.from({ length: 22 }, (_, index) => ({
index: index + 1,
checkpointId: `checkpoint-${index}`,
nodeName: `node-${index}`,
phase: '其它',
status: 'completed',
durationMs: 1000,
durationText: '1s',
retryCount: 0,
createdAt: '2026-09-01T00:00:00Z'
}))
}
]
const metrics = {
projectId: 'diagnostics-test',
nodeCount: 22,
totalDurationText: '22s',
retryCount: 1,
successRate: 100,
failedNodeCount: 0
}
describe('运行观测入口', () => {
it('关闭时不请求,打开只读两个接口,完整展示超过 18 条记录且不伪造成功率', async () => {
const fetcher = vi
.fn<typeof fetch>()
.mockImplementation(
async url => new Response(JSON.stringify({ data: String(url).endsWith('/metrics') ? metrics : groups }))
)
vi.stubGlobal('fetch', fetcher)
const checkpoints: Checkpoint[] = [
{
checkpointId: 'checkpoint-0',
workflowName: 'breakdown',
createdAt: '2026-09-01T00:00:00Z',
state: {
workflowExecution: {
status: 'failed',
executionId: 'execution-1',
startedAt: '2026-09-01T00:00:00Z'
}
}
}
]
wrapper = mount(WorkflowDiagnosticsDialog, {
attachTo: document.body,
props: { projectId: 'diagnostics-test', open: false, checkpoints }
})
await flushPromises()
expect(fetcher).not.toHaveBeenCalled()
await wrapper.setProps({ open: true })
await flushPromises()
expect(fetcher).toHaveBeenCalledTimes(2)
expect(fetcher.mock.calls.every(([, init]) => init?.method === 'GET')).toBe(true)
expect(document.querySelectorAll('.diagnostics-records article')).toHaveLength(22)
expect(document.body.textContent).toContain('包含失败记录')
expect(document.body.textContent).not.toContain('100%')
const input = document.querySelector<HTMLInputElement>('[aria-label="搜索运行记录"]')!
input.value = 'checkpoint-21'
input.dispatchEvent(new Event('input', { bubbles: true }))
await flushPromises()
expect(document.querySelectorAll('.diagnostics-records article')).toHaveLength(1)
})
it('一个观测接口失败仍显示另一项,不启动恢复或生产', async () => {
const fetcher = vi
.fn<typeof fetch>()
.mockImplementation(async url =>
String(url).endsWith('/metrics')
? new Response(JSON.stringify({ error: '指标不可用' }), { status: 500 })
: new Response(JSON.stringify({ data: groups }))
)
vi.stubGlobal('fetch', fetcher)
const result = await loadWorkflowDiagnostics('project/1')
expect(result.metrics).toBeNull()
expect(result.groups?.[0]?.nodes).toHaveLength(22)
expect(result.errors[0]).toContain('指标')
expect(fetcher.mock.calls[0]?.[0]).toBe('/api/projects/project%2F1/metrics')
expect(fetcher.mock.calls[1]?.[0]).toBe('/api/projects/project%2F1/timeline/grouped')
expect(fetcher.mock.calls.every(([, init]) => init?.method === 'GET')).toBe(true)
})
it('指标项目不匹配时拒绝展示', async () => {
vi.stubGlobal(
'fetch',
vi
.fn<typeof fetch>()
.mockImplementation(
async url => new Response(JSON.stringify({ data: String(url).endsWith('/metrics') ? metrics : [] }))
)
)
const result = await loadWorkflowDiagnostics('other-project')
expect(result.metrics).toBeNull()
expect(result.errors.join()).toContain('不匹配的项目')
})
it('切项目会取消旧查询,晚到响应不会写进新项目', async () => {
const pending: ((response: Response) => void)[] = []
const fetcher = vi.fn<typeof fetch>().mockImplementation(url =>
String(url).includes('/old/')
? new Promise(resolve => pending.push(resolve))
: Promise.resolve(
new Response(
JSON.stringify({
data: String(url).endsWith('/metrics')
? { ...metrics, projectId: 'new', nodeCount: 0 }
: []
})
)
)
)
vi.stubGlobal('fetch', fetcher)
wrapper = mount(WorkflowDiagnosticsDialog, {
attachTo: document.body,
props: { projectId: 'old', open: true, checkpoints: [] }
})
await flushPromises()
const signal = fetcher.mock.calls[0]?.[1]?.signal
await wrapper.setProps({ projectId: 'new' })
await flushPromises()
expect(signal?.aborted).toBe(true)
pending[0]!(new Response(JSON.stringify({ data: { ...metrics, projectId: 'old' } })))
pending[1]!(new Response(JSON.stringify({ data: groups })))
await flushPromises()
expect(document.querySelectorAll('.diagnostics-records article')).toHaveLength(0)
expect(document.body.textContent).toContain('全项目记录 0')
await wrapper.setProps({ open: false })
await flushPromises()
expect(fetcher).toHaveBeenCalledTimes(4)
})
it('关闭弹窗时取消仍未返回的查询', async () => {
const finishes: ((response: Response) => void)[] = []
const fetcher = vi.fn<typeof fetch>().mockImplementation(() => new Promise(resolve => finishes.push(resolve)))
vi.stubGlobal('fetch', fetcher)
wrapper = mount(WorkflowDiagnosticsDialog, {
attachTo: document.body,
props: { projectId: 'diagnostics-test', open: true, checkpoints: [] }
})
await flushPromises()
await wrapper.setProps({ open: false })
await flushPromises()
expect(fetcher.mock.calls.every(([, init]) => init?.signal?.aborted)).toBe(true)
finishes[0]!(new Response(JSON.stringify({ data: metrics })))
finishes[1]!(new Response(JSON.stringify({ data: groups })))
await flushPromises()
expect(fetcher).toHaveBeenCalledTimes(2)
})
})