feat: focus collection page on account activity and progress

This commit is contained in:
2026-10-13 16:52:09 +08:00
parent 1de73b5f1c
commit bda0743520
14 changed files with 695 additions and 166 deletions
@@ -0,0 +1,82 @@
import { mount } from '@vue/test-utils'
import { afterEach, describe, expect, it, vi } from 'vitest'
import CollectionTaskProgress from './CollectionTaskProgress.vue'
import CollectionProgressGauge from './CollectionProgressGauge.vue'
import type { Task, TaskDetail, StageEstimate, WarehouseOverview } from '../types'
const task: Task = { taskNo: 'TASK-W', warehouseCode: 'W', taskType: 'BACKFILL', dataFamily: 'OUTBOUND', taskStatus: 'WAITING_SOURCE', dependencyState: 'WAITING_CONFIRMED', progressSourceTaskNo: 'TASK-S', queryFrom: null, queryUntil: null, applyState: null, attemptCount: 0, latestAttemptNo: null, resultPublicationState: 'NOT_PUBLISHED', nextRetryAt: null, allowedActions: [], actionBlockReason: null, progress: null }
const progress = { phase: 'DETAIL', progressScope: 'ACCOUNT_SHARED', countBasis: 'TASK_CUMULATIVE', attemptNo: 2, scanRound: 1, completed: 339, total: 2787, totalKnown: true, unit: 'ORDER', sliceFrom: null, sliceUntil: null, progressUpdatedAt: '2026-10-13T01:01:00Z' }
const source: Task = { ...task, taskNo: 'TASK-S', taskStatus: 'RUNNING', dependencyState: 'NONE', progressSourceTaskNo: null, attemptCount: 2, progress }
const detail = { task, sharedUpstream: source, observedAt: '2026-10-13T01:02:00Z', protection: {} } as TaskDetail
const wrappers: ReturnType<typeof mount>[] = []
afterEach(() => { wrappers.splice(0).forEach(w => w.unmount()); vi.useRealTimers() })
describe('collection task progress evidence', () => {
it('uses only a confirmed source and does not infer earlier stage completion from the current phase', async () => {
const w = mount(CollectionTaskProgress, { props: { task, detail } }); wrappers.push(w)
expect(w.text()).toContain('TASK-S'); expect(w.text()).toContain('12.1%'); expect(w.text()).toContain('2448')
expect(w.text()).toContain('账号共享'); expect(w.findAll('li')[0]?.text()).toContain('状态未确认')
expect(w.text()).not.toContain('✓')
await w.setProps({ task: { ...task, dependencyState: 'WAITING_UNCONFIRMED', progressSourceTaskNo: null } })
expect(w.text()).toContain('关联进度尚未确认'); expect(w.text()).not.toContain('339'); expect(w.text()).not.toContain('12.1%')
})
it('accepts only explicit matching stage completion and keeps publication independent', () => {
const w = mount(CollectionTaskProgress, { props: { task, detail: { ...detail, sharedUpstream: { ...source, progress: { ...progress, stages: [{ ...progress, phase: 'DIRECTORY', stageFinishedAt: '2026-10-13T01:00:00Z' }, { ...progress, phase: 'PUBLISH', stageFinishedAt: '2026-10-13T01:00:00Z' }] } } } } }); wrappers.push(w)
expect(w.findAll('li')[0]?.text()).toContain('✓ 已结束')
expect(w.findAll('li')[4]?.text()).not.toContain('✓')
expect(w.text()).toContain('报表可用状态待确认')
})
it('retains confirmed source evidence after dependency failure without borrowing its current count', () => {
const w = mount(CollectionTaskProgress, { props: { task: { ...task, dependencyState: 'FAILED', progressSourceTaskNo: task.taskNo, blockingReason: 'UPSTREAM_FAILED' }, detail } }); wrappers.push(w)
expect(w.text()).toContain('已确认关联来源:TASK-S')
expect(w.text()).toContain('关联来源失败,等待恢复')
expect(w.text()).not.toContain('12.1%')
})
it('treats COUNT_COMPLETE including zero of zero only as completed counts', () => {
const stageEstimate: StageEstimate = { status: 'UNAVAILABLE', progressIdentity: 'zero-count', completed: '0', total: '0', sampleFrom: null, sampleUntil: null, completedDelta: null, ratePerSecond: null, remainingSeconds: null, estimatedAt: null, validUntil: null, unavailableReason: 'COUNT_COMPLETE' }
const w = mount(CollectionTaskProgress, { props: { task, visible: true, detail: { ...detail, sharedUpstream: { ...source, progress: { ...progress, completed: 0, total: 0 }, stageEstimate } } } }); wrappers.push(w)
expect(w.text()).toContain('计数已齐,等待阶段确认')
expect(w.text()).not.toContain('✓ 已结束')
expect(w.text()).not.toContain('预计还需')
expect(w.find('[role="progressbar"]').exists()).toBe(false)
})
it('uses unavailable reasons as the primary estimate text and prioritizes explicit stage evidence', async () => {
const e: StageEstimate = { status: 'UNAVAILABLE', progressIdentity: 'identity', completed: 339, total: 2787, sampleFrom: null, sampleUntil: null, completedDelta: null, ratePerSecond: null, remainingSeconds: null, estimatedAt: null, validUntil: null, unavailableReason: 'TERMINAL' }
const w = mount(CollectionTaskProgress, { props: { task, visible: true, detail: { ...detail, sharedUpstream: { ...source, stageEstimate: e } } } }); wrappers.push(w)
for (const [reason, expected] of [['TERMINAL', '任务已结束,不再估算'], ['COUNT_COMPLETE', '计数已齐,等待阶段确认'], ['NO_RECENT_PROGRESS', '近期无新增进度,暂不估算'], ['SAMPLING', '正在测速,可靠样本尚未齐全'], ['UNRECOGNIZED', '估算依据待确认']]) {
await w.setProps({ detail: { ...detail, sharedUpstream: { ...source, stageEstimate: { ...e, unavailableReason: reason } } } })
expect(w.find('.collection-estimate strong').text()).toBe(expected)
}
await w.setProps({ detail: { ...detail, sharedUpstream: { ...source, progress: { ...progress, stageFinishedAt: '2026-10-13T01:02:00Z' }, stageEstimate: { ...e, unavailableReason: 'SAMPLING' } } } })
expect(w.find('.collection-estimate strong').text()).toContain('本阶段已结束')
expect(w.text()).not.toContain('正在测速')
})
it('distinguishes unobserved warehouse metadata from a confirmed missing publication', async () => {
const w = mount(CollectionTaskProgress, { props: { task, detail } }); wrappers.push(w)
expect(w.text()).toContain('报表可用状态待确认')
expect(w.text()).not.toContain('尚无可用数据')
await w.setProps({ warehouse: { lastPublishedAt: null } as WarehouseOverview })
expect(w.text()).toContain('尚无可用数据')
expect(w.text()).not.toContain('报表可用状态待确认')
await w.setProps({ warehouse: { lastPublishedAt: '2026-10-13T01:00:00Z' } as WarehouseOverview })
expect(w.text()).toContain('已发布数据')
expect(w.text()).not.toContain('尚无可用数据')
})
it('withdraws ETA when expired, count identity changes, hidden, or reads fail', async () => {
vi.useFakeTimers(); vi.setSystemTime(new Date('2026-10-13T01:02:00Z'))
const stageEstimate: StageEstimate = { status: 'AVAILABLE', progressIdentity: 'source-attempt-2-detail-1', completed: 339, total: 2787, sampleFrom: '2026-10-13T01:00:00Z', sampleUntil: '2026-10-13T01:02:00Z', completedDelta: 60, ratePerSecond: 0.5, remainingSeconds: 4896, estimatedAt: '2026-10-13T01:02:00Z', validUntil: '2026-10-13T01:02:10Z', unavailableReason: null }
const d = { ...detail, sharedUpstream: { ...source, stageEstimate } }
const w = mount(CollectionTaskProgress, { props: { task, detail: d, visible: true } }); wrappers.push(w)
expect(w.text()).toContain('约82分钟')
await w.setProps({ stale: true }); expect(w.text()).not.toContain('约82分钟'); expect(w.text()).toContain('状态更新失败')
await w.setProps({ stale: false, visible: false }); expect(w.text()).not.toContain('约82分钟')
await w.setProps({ visible: true, detail: { ...d, sharedUpstream: { ...d.sharedUpstream, progress: { ...progress, completed: 340 } } } }); expect(w.text()).not.toContain('约82分钟')
await w.setProps({ detail: d }); await vi.advanceTimersByTimeAsync(10001); expect(w.text()).toContain('估算已过期'); expect(w.text()).not.toContain('约82分钟')
})
it('never paints a percentage for unknown, zero, invalid, or changing denominators', async () => {
const w = mount(CollectionProgressGauge, { props: { taskNo: source.taskNo, progress } }); wrappers.push(w)
expect(w.find('[role="progressbar"]').attributes('aria-valuenow')).toBe('12.1')
await w.setProps({ progress: { ...progress, total: 3000 } }); expect(w.find('[role="progressbar"]').exists()).toBe(false); expect(w.text()).toContain('正在复核')
await w.setProps({ progress: { ...progress, attemptNo: 3, totalKnown: false } }); expect(w.find('[role="progressbar"]').exists()).toBe(false)
await w.setProps({ progress: { ...progress, attemptNo: 4, total: 0, completed: 0 } }); expect(w.text()).toContain('本阶段数量为零'); expect(w.find('[role="progressbar"]').exists()).toBe(false)
await w.setProps({ progress: { ...progress, attemptNo: 5, total: 1, completed: 2 } }); expect(w.find('[role="progressbar"]').exists()).toBe(false)
})
})