feat: improve cleaner history pagination and report states
This commit is contained in:
@@ -6,7 +6,7 @@
|
|||||||
* Admin users see all users' records, regular users see only their own.
|
* Admin users see all users' records, regular users see only their own.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState, useEffect, useCallback } from 'react'
|
import React, { useState, useEffect, useCallback, useTransition } from 'react'
|
||||||
import { Modal } from './ui/Modal'
|
import { Modal } from './ui/Modal'
|
||||||
import { useLogger } from '../hooks/useLogger'
|
import { useLogger } from '../hooks/useLogger'
|
||||||
import {
|
import {
|
||||||
@@ -69,6 +69,8 @@ interface BatchItemProps {
|
|||||||
onDelete: (batchId: string) => void
|
onDelete: (batchId: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const BATCH_PAGE_SIZE = 5
|
||||||
|
|
||||||
const statusStyles: Record<string, string> = {
|
const statusStyles: Record<string, string> = {
|
||||||
success: 'bg-green-100 text-green-700',
|
success: 'bg-green-100 text-green-700',
|
||||||
partial: 'bg-amber-100 text-amber-700',
|
partial: 'bg-amber-100 text-amber-700',
|
||||||
@@ -286,12 +288,15 @@ const BatchItem = React.memo(({ batch, isAdmin, onDelete }: BatchItemProps) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const filteredOrders =
|
||||||
|
currentAttempt !== undefined ? orders.filter((order) => order.attemptNumber === currentAttempt) : orders
|
||||||
|
const visibleExecutions =
|
||||||
|
currentAttempt !== undefined
|
||||||
|
? executions.filter((execution) => execution.attemptNumber === currentAttempt)
|
||||||
|
: executions
|
||||||
|
|
||||||
const handleCopyColumn = (field: keyof CleanerHistoryOrderRecord) => {
|
const handleCopyColumn = (field: keyof CleanerHistoryOrderRecord) => {
|
||||||
const filtered =
|
const values = filteredOrders
|
||||||
currentAttempt !== undefined
|
|
||||||
? orders.filter((o) => o.attemptNumber === currentAttempt)
|
|
||||||
: orders
|
|
||||||
const values = filtered
|
|
||||||
.map((o) => String(o[field] ?? ''))
|
.map((o) => String(o[field] ?? ''))
|
||||||
.filter((v) => v && v !== '-')
|
.filter((v) => v && v !== '-')
|
||||||
.join('\n')
|
.join('\n')
|
||||||
@@ -307,9 +312,6 @@ const BatchItem = React.memo(({ batch, isAdmin, onDelete }: BatchItemProps) => {
|
|||||||
.catch(() => showError('复制失败,请手动复制'))
|
.catch(() => showError('复制失败,请手动复制'))
|
||||||
}
|
}
|
||||||
|
|
||||||
const filteredOrders =
|
|
||||||
currentAttempt !== undefined ? orders.filter((o) => o.attemptNumber === currentAttempt) : orders
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="border border-gray-200 rounded-lg overflow-hidden">
|
<div className="border border-gray-200 rounded-lg overflow-hidden">
|
||||||
{/* Batch summary */}
|
{/* Batch summary */}
|
||||||
@@ -420,32 +422,30 @@ const BatchItem = React.memo(({ batch, isAdmin, onDelete }: BatchItemProps) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex flex-wrap gap-4 text-xs text-gray-600">
|
<div className="flex flex-wrap gap-4 text-xs text-gray-600">
|
||||||
{executions
|
{visibleExecutions.map((exec) => (
|
||||||
.filter((e) => currentAttempt === undefined || e.attemptNumber === currentAttempt)
|
<React.Fragment key={exec.attemptNumber}>
|
||||||
.map((exec) => (
|
<span>耗时:{formatDuration(exec.operationTime, exec.endTime)}</span>
|
||||||
<React.Fragment key={exec.attemptNumber}>
|
<span>
|
||||||
<span>耗时:{formatDuration(exec.operationTime, exec.endTime)}</span>
|
订单:{exec.ordersProcessed}/{exec.totalOrders}
|
||||||
<span>
|
</span>
|
||||||
订单:{exec.ordersProcessed}/{exec.totalOrders}
|
<span>删除:{exec.totalMaterialsDeleted}</span>
|
||||||
|
{exec.totalMaterialsFailed > 0 && (
|
||||||
|
<span className="text-red-600">失败:{exec.totalMaterialsFailed}</span>
|
||||||
|
)}
|
||||||
|
{exec.totalUncertainDeletions > 0 && (
|
||||||
|
<span className="text-amber-600">
|
||||||
|
不确定:{exec.totalUncertainDeletions}
|
||||||
</span>
|
</span>
|
||||||
<span>删除:{exec.totalMaterialsDeleted}</span>
|
)}
|
||||||
{exec.totalMaterialsFailed > 0 && (
|
{exec.errorMessage && (
|
||||||
<span className="text-red-600">失败:{exec.totalMaterialsFailed}</span>
|
<span className="text-red-600" title={exec.errorMessage}>
|
||||||
)}
|
错误:{exec.errorMessage.substring(0, 80)}
|
||||||
{exec.totalUncertainDeletions > 0 && (
|
{exec.errorMessage.length > 80 ? '...' : ''}
|
||||||
<span className="text-amber-600">
|
</span>
|
||||||
不确定:{exec.totalUncertainDeletions}
|
)}
|
||||||
</span>
|
{exec.appVersion && <span className="text-gray-400">v{exec.appVersion}</span>}
|
||||||
)}
|
</React.Fragment>
|
||||||
{exec.errorMessage && (
|
))}
|
||||||
<span className="text-red-600" title={exec.errorMessage}>
|
|
||||||
错误:{exec.errorMessage.substring(0, 80)}
|
|
||||||
{exec.errorMessage.length > 80 ? '...' : ''}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{exec.appVersion && <span className="text-gray-400">v{exec.appVersion}</span>}
|
|
||||||
</React.Fragment>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -711,9 +711,13 @@ export const CleanerOperationHistoryModal: React.FC<CleanerOperationHistoryModal
|
|||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
const [allUsers, setAllUsers] = useState<string[]>([])
|
const [allUsers, setAllUsers] = useState<string[]>([])
|
||||||
const [selectedUsers, setSelectedUsers] = useState<string[]>([])
|
const [selectedUsers, setSelectedUsers] = useState<string[]>([])
|
||||||
|
const [currentPage, setCurrentPage] = useState(0)
|
||||||
|
const [isFilterPending, startFilterTransition] = useTransition()
|
||||||
const logger = useLogger('CleanerOperationHistory')
|
const logger = useLogger('CleanerOperationHistory')
|
||||||
|
|
||||||
const isAdmin = user?.userType === 'Admin'
|
const isAdmin = user?.userType === 'Admin'
|
||||||
|
const hasPreviousPage = currentPage > 0
|
||||||
|
const hasNextPage = batches.length === BATCH_PAGE_SIZE
|
||||||
|
|
||||||
const fetchBatches = useCallback(async () => {
|
const fetchBatches = useCallback(async () => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
@@ -721,8 +725,12 @@ export const CleanerOperationHistoryModal: React.FC<CleanerOperationHistoryModal
|
|||||||
try {
|
try {
|
||||||
const options =
|
const options =
|
||||||
isAdmin && selectedUsers.length > 0
|
isAdmin && selectedUsers.length > 0
|
||||||
? { limit: 100, usernames: selectedUsers }
|
? {
|
||||||
: { limit: 100 }
|
limit: BATCH_PAGE_SIZE,
|
||||||
|
offset: currentPage * BATCH_PAGE_SIZE,
|
||||||
|
usernames: selectedUsers
|
||||||
|
}
|
||||||
|
: { limit: BATCH_PAGE_SIZE, offset: currentPage * BATCH_PAGE_SIZE }
|
||||||
|
|
||||||
const result = await window.electron.cleaner.getHistoryBatches(options)
|
const result = await window.electron.cleaner.getHistoryBatches(options)
|
||||||
if (result.success && result.data) {
|
if (result.success && result.data) {
|
||||||
@@ -735,7 +743,7 @@ export const CleanerOperationHistoryModal: React.FC<CleanerOperationHistoryModal
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}, [isAdmin, selectedUsers])
|
}, [currentPage, isAdmin, selectedUsers])
|
||||||
|
|
||||||
const fetchAllUsers = useCallback(async () => {
|
const fetchAllUsers = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -765,13 +773,28 @@ export const CleanerOperationHistoryModal: React.FC<CleanerOperationHistoryModal
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const toggleUserFilter = (username: string) => {
|
const toggleUserFilter = (username: string) => {
|
||||||
setSelectedUsers((prev) =>
|
startFilterTransition(() => {
|
||||||
prev.includes(username) ? prev.filter((u) => u !== username) : [...prev, username]
|
setCurrentPage(0)
|
||||||
)
|
setSelectedUsers((prev) =>
|
||||||
|
prev.includes(username) ? prev.filter((u) => u !== username) : [...prev, username]
|
||||||
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const clearUserFilters = () => {
|
const clearUserFilters = () => {
|
||||||
setSelectedUsers([])
|
startFilterTransition(() => {
|
||||||
|
setCurrentPage(0)
|
||||||
|
setSelectedUsers([])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToPreviousPage = () => {
|
||||||
|
setCurrentPage((prev) => Math.max(0, prev - 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToNextPage = () => {
|
||||||
|
if (!hasNextPage) return
|
||||||
|
setCurrentPage((prev) => prev + 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isOpen) return null
|
if (!isOpen) return null
|
||||||
@@ -833,8 +856,9 @@ export const CleanerOperationHistoryModal: React.FC<CleanerOperationHistoryModal
|
|||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
{batches.length > 0 && (
|
{batches.length > 0 && (
|
||||||
<span className="text-sm text-gray-500">共 {batches.length} 条批次</span>
|
<span className="text-sm text-gray-500">本页 {batches.length} 条批次</span>
|
||||||
)}
|
)}
|
||||||
|
{isFilterPending && <span className="text-sm text-blue-600">正在更新筛选...</span>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@@ -875,14 +899,26 @@ export const CleanerOperationHistoryModal: React.FC<CleanerOperationHistoryModal
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
{/* Footer */}
|
<div className="pt-4 border-t border-gray-200 flex justify-center">
|
||||||
<div className="pt-4 border-t border-gray-200 flex justify-end">
|
<div className="inline-flex items-center rounded-full border border-slate-200 bg-white p-1 shadow-sm">
|
||||||
<button
|
<button
|
||||||
className="px-6 py-2 rounded-md bg-gray-100 hover:bg-gray-200 text-gray-700 font-medium transition-colors"
|
className="inline-flex items-center rounded-full px-4 py-2 text-sm font-medium text-slate-700 transition-colors hover:bg-slate-100 disabled:cursor-not-allowed disabled:text-slate-300"
|
||||||
onClick={onClose}
|
onClick={goToPreviousPage}
|
||||||
>
|
disabled={loading || !hasPreviousPage}
|
||||||
关闭
|
>
|
||||||
</button>
|
上一页
|
||||||
|
</button>
|
||||||
|
<div className="mx-1 min-w-[5.5rem] rounded-full bg-slate-900 px-4 py-2 text-center text-sm font-semibold text-white">
|
||||||
|
第 {currentPage + 1} 页
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="inline-flex items-center rounded-full px-4 py-2 text-sm font-medium text-slate-700 transition-colors hover:bg-slate-100 disabled:cursor-not-allowed disabled:text-slate-300"
|
||||||
|
onClick={goToNextPage}
|
||||||
|
disabled={loading || !hasNextPage}
|
||||||
|
>
|
||||||
|
下一页
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import React from 'react'
|
|||||||
import { CheckCircle, XCircle, SkipForward, Package, Loader2, AlertTriangle } from 'lucide-react'
|
import { CheckCircle, XCircle, SkipForward, Package, Loader2, AlertTriangle } from 'lucide-react'
|
||||||
import { Modal } from './ui/Modal'
|
import { Modal } from './ui/Modal'
|
||||||
import type { CleanerProgress } from '../hooks/cleaner/types'
|
import type { CleanerProgress } from '../hooks/cleaner/types'
|
||||||
|
import { getExecutionReportState } from './execution-report-state'
|
||||||
|
|
||||||
interface ExecutionReportDialogProps {
|
interface ExecutionReportDialogProps {
|
||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
@@ -56,6 +57,12 @@ export const ExecutionReportDialog: React.FC<ExecutionReportDialogProps> = ({
|
|||||||
const hasFailedMaterials = materialsFailed > 0 || uncertainDeletions > 0
|
const hasFailedMaterials = materialsFailed > 0 || uncertainDeletions > 0
|
||||||
const showProgress = isExecuting && progress
|
const showProgress = isExecuting && progress
|
||||||
const isProgressing = !!showProgress
|
const isProgressing = !!showProgress
|
||||||
|
const reportState = getExecutionReportState({
|
||||||
|
dryRun,
|
||||||
|
errors,
|
||||||
|
materialsFailed,
|
||||||
|
uncertainDeletions
|
||||||
|
})
|
||||||
|
|
||||||
// Update timer during progress
|
// Update timer during progress
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@@ -123,11 +130,7 @@ export const ExecutionReportDialog: React.FC<ExecutionReportDialogProps> = ({
|
|||||||
title={
|
title={
|
||||||
isProgressing
|
isProgressing
|
||||||
? '正在执行清理...'
|
? '正在执行清理...'
|
||||||
: dryRun
|
: reportState.title
|
||||||
? '预览执行报告'
|
|
||||||
: hasErrors
|
|
||||||
? '执行完成 (有错误)'
|
|
||||||
: '执行完成'
|
|
||||||
}
|
}
|
||||||
size={isProgressing ? 'lg' : 'md'}
|
size={isProgressing ? 'lg' : 'md'}
|
||||||
showCloseButton={!isExecuting}
|
showCloseButton={!isExecuting}
|
||||||
@@ -161,19 +164,17 @@ export const ExecutionReportDialog: React.FC<ExecutionReportDialogProps> = ({
|
|||||||
<div className="flex justify-center mb-4">
|
<div className="flex justify-center mb-4">
|
||||||
{dryRun ? (
|
{dryRun ? (
|
||||||
<Package className="w-12 h-12 text-amber-500" />
|
<Package className="w-12 h-12 text-amber-500" />
|
||||||
) : hasErrors ? (
|
) : reportState.state === 'failure' ? (
|
||||||
<XCircle className="w-12 h-12 text-red-500" />
|
<XCircle className="w-12 h-12 text-red-500" />
|
||||||
|
) : reportState.state === 'partial_success' ? (
|
||||||
|
<AlertTriangle className="w-12 h-12 text-amber-500" />
|
||||||
|
) : reportState.state === 'manual_review' ? (
|
||||||
|
<AlertTriangle className="w-12 h-12 text-yellow-500" />
|
||||||
) : (
|
) : (
|
||||||
<CheckCircle className="w-12 h-12 text-green-500" />
|
<CheckCircle className="w-12 h-12 text-green-500" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-gray-600">
|
<p className="text-sm text-gray-600">{reportState.summary}</p>
|
||||||
{dryRun
|
|
||||||
? '预览模式 - 未实际删除数据'
|
|
||||||
: hasErrors
|
|
||||||
? '部分操作未能完成,请查看下方错误信息'
|
|
||||||
: '所有操作已成功完成'}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -405,19 +406,33 @@ export const ExecutionReportDialog: React.FC<ExecutionReportDialogProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!hasErrors && !dryRun && (
|
{reportState.showSuccessBanner && (
|
||||||
<div className="flex items-center justify-center gap-2 p-3 bg-green-50 rounded-lg border border-green-200 text-green-700 text-sm">
|
<div className="flex items-center justify-center gap-2 p-3 bg-green-50 rounded-lg border border-green-200 text-green-700 text-sm">
|
||||||
<CheckCircle size={16} className="flex-shrink-0" />
|
<CheckCircle size={16} className="flex-shrink-0" />
|
||||||
<span>操作已成功完成,数据已同步到 ERP 系统</span>
|
<span>操作已成功完成,数据已同步到 ERP 系统</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!hasErrors && dryRun && (
|
{reportState.showPreviewBanner && (
|
||||||
<div className="flex items-center justify-center gap-2 p-3 bg-amber-50 rounded-lg border border-amber-200 text-amber-700 text-sm">
|
<div className="flex items-center justify-center gap-2 p-3 bg-amber-50 rounded-lg border border-amber-200 text-amber-700 text-sm">
|
||||||
<Package size={16} className="flex-shrink-0" />
|
<Package size={16} className="flex-shrink-0" />
|
||||||
<span>预览模式结束,数据未实际修改。确认无误后可正式执行。</span>
|
<span>预览模式结束,数据未实际修改。确认无误后可正式执行。</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{reportState.state === 'partial_success' && (
|
||||||
|
<div className="flex items-center justify-center gap-2 p-3 bg-amber-50 rounded-lg border border-amber-200 text-amber-700 text-sm">
|
||||||
|
<AlertTriangle size={16} className="flex-shrink-0" />
|
||||||
|
<span>存在删除失败的物料,本次执行未完全成功,建议结合历史记录继续排查。</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{reportState.state === 'manual_review' && (
|
||||||
|
<div className="flex items-center justify-center gap-2 p-3 bg-yellow-50 rounded-lg border border-yellow-200 text-yellow-700 text-sm">
|
||||||
|
<AlertTriangle size={16} className="flex-shrink-0" />
|
||||||
|
<span>存在不确定删除结果,请人工确认 ERP 中的最终状态后再继续后续操作。</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
80
src/renderer/src/components/execution-report-state.ts
Normal file
80
src/renderer/src/components/execution-report-state.ts
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
export type ExecutionReportResultState =
|
||||||
|
| 'preview'
|
||||||
|
| 'success'
|
||||||
|
| 'partial_success'
|
||||||
|
| 'manual_review'
|
||||||
|
| 'failure'
|
||||||
|
|
||||||
|
export interface ExecutionReportStateInput {
|
||||||
|
dryRun?: boolean
|
||||||
|
errors?: string[]
|
||||||
|
materialsFailed?: number
|
||||||
|
uncertainDeletions?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExecutionReportStateDescriptor {
|
||||||
|
state: ExecutionReportResultState
|
||||||
|
title: string
|
||||||
|
summary: string
|
||||||
|
showSuccessBanner: boolean
|
||||||
|
showPreviewBanner: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getExecutionReportState(
|
||||||
|
input: ExecutionReportStateInput
|
||||||
|
): ExecutionReportStateDescriptor {
|
||||||
|
const {
|
||||||
|
dryRun = false,
|
||||||
|
errors = [],
|
||||||
|
materialsFailed = 0,
|
||||||
|
uncertainDeletions = 0
|
||||||
|
} = input
|
||||||
|
|
||||||
|
if (dryRun) {
|
||||||
|
return {
|
||||||
|
state: 'preview',
|
||||||
|
title: '预览执行报告',
|
||||||
|
summary: '预览模式 - 未实际删除数据',
|
||||||
|
showSuccessBanner: false,
|
||||||
|
showPreviewBanner: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.length > 0) {
|
||||||
|
return {
|
||||||
|
state: 'failure',
|
||||||
|
title: '执行完成 (失败)',
|
||||||
|
summary: '执行过程中出现错误,请先处理错误后再继续。',
|
||||||
|
showSuccessBanner: false,
|
||||||
|
showPreviewBanner: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (materialsFailed > 0) {
|
||||||
|
return {
|
||||||
|
state: 'partial_success',
|
||||||
|
title: '执行完成 (部分成功)',
|
||||||
|
summary: '部分物料删除失败,请结合下方统计和历史记录继续排查。',
|
||||||
|
showSuccessBanner: false,
|
||||||
|
showPreviewBanner: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uncertainDeletions > 0) {
|
||||||
|
return {
|
||||||
|
state: 'manual_review',
|
||||||
|
title: '执行完成 (需人工确认)',
|
||||||
|
summary: '存在不确定删除结果,请人工复核后再判断是否完成。',
|
||||||
|
showSuccessBanner: false,
|
||||||
|
showPreviewBanner: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
state: 'success',
|
||||||
|
title: '执行完成',
|
||||||
|
summary: '所有操作已成功完成',
|
||||||
|
showSuccessBanner: true,
|
||||||
|
showPreviewBanner: false
|
||||||
|
}
|
||||||
|
}
|
||||||
60
tests/unit/execution-report-state.test.ts
Normal file
60
tests/unit/execution-report-state.test.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import { getExecutionReportState } from '../../src/renderer/src/components/execution-report-state'
|
||||||
|
|
||||||
|
describe('execution report state helpers', () => {
|
||||||
|
it('treats dry-run as preview regardless of counters', () => {
|
||||||
|
const state = getExecutionReportState({
|
||||||
|
dryRun: true,
|
||||||
|
errors: ['should be ignored'],
|
||||||
|
materialsFailed: 1,
|
||||||
|
uncertainDeletions: 1
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(state.state).toBe('preview')
|
||||||
|
expect(state.title).toBe('预览执行报告')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('treats runtime errors as failure', () => {
|
||||||
|
const state = getExecutionReportState({
|
||||||
|
errors: ['boom'],
|
||||||
|
materialsFailed: 0,
|
||||||
|
uncertainDeletions: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(state.state).toBe('failure')
|
||||||
|
expect(state.title).toBe('执行完成 (失败)')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('treats failed materials as partial success when there are no runtime errors', () => {
|
||||||
|
const state = getExecutionReportState({
|
||||||
|
errors: [],
|
||||||
|
materialsFailed: 3,
|
||||||
|
uncertainDeletions: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(state.state).toBe('partial_success')
|
||||||
|
expect(state.showSuccessBanner).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('treats uncertain deletions as manual review when everything else succeeded', () => {
|
||||||
|
const state = getExecutionReportState({
|
||||||
|
errors: [],
|
||||||
|
materialsFailed: 0,
|
||||||
|
uncertainDeletions: 2
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(state.state).toBe('manual_review')
|
||||||
|
expect(state.showSuccessBanner).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('treats clean completion as success', () => {
|
||||||
|
const state = getExecutionReportState({
|
||||||
|
errors: [],
|
||||||
|
materialsFailed: 0,
|
||||||
|
uncertainDeletions: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(state.state).toBe('success')
|
||||||
|
expect(state.showSuccessBanner).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user