167 lines
7.2 KiB
TypeScript
167 lines
7.2 KiB
TypeScript
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)
|
|
})
|
|
})
|