feat: 同步报销流程与工作台改动

This commit is contained in:
caoxiaozhu
2026-06-09 08:32:00 +00:00
parent e124e4bbcb
commit 25724c354f
64 changed files with 6518 additions and 687 deletions

View File

@@ -87,7 +87,7 @@ test('platform admin users do not enter the personal workbench', () => {
assert.equal(canAccessAppView(adminUser, 'workbench'), false)
assert.equal(canAccessAppView(employeeUser, 'workbench'), true)
assert.equal(getAccessibleViewIds(adminUser).includes('workbench'), false)
assert.deepEqual(resolveDefaultAuthorizedRoute(adminUser), { name: 'app-overview' })
assert.deepEqual(resolveDefaultAuthorizedRoute(adminUser), { name: 'app-documents' })
assert.deepEqual(
filterNavItemsByAccess(navItems, adminUser).map((item) => item.id),
['documents', 'overview', 'settings']
@@ -201,6 +201,30 @@ test('direct-manager approval helpers only match claims pushed to the current us
assert.equal(isCurrentDirectManagerForRequest({ person: '张三', managerName: '王总' }, managerUser), false)
})
test('approver executive users can process claims routed to their direct-manager identity', () => {
const leaderUser = {
roleCodes: ['approver', 'executive'],
name: 'Xiang Wanhong',
username: 'xiangwanhong@xf.com'
}
assert.equal(canApproveLeaderExpenseClaims(leaderUser), true)
assert.equal(
isCurrentDirectManagerForRequest(
{ person: 'Shen Zhiyuan', managerName: 'Xiang Wanhong' },
leaderUser
),
true
)
assert.equal(
isCurrentDirectManagerForRequest(
{ person: 'Xiang Wanhong', managerName: 'Li Wenjing' },
leaderUser
),
false
)
})
test('applicant helper matches generated draft owner by employee identifiers', () => {
const currentUser = {
username: 'caoxiaozhu@xf.com',

View File

@@ -27,6 +27,10 @@ const appShellComposable = readFileSync(
fileURLToPath(new URL('../src/composables/useAppShell.js', import.meta.url)),
'utf8'
)
const requestsComposable = readFileSync(
fileURLToPath(new URL('../src/composables/useRequests.js', import.meta.url)),
'utf8'
)
const assistantScript = readFileSync(
fileURLToPath(new URL('../src/views/scripts/TravelReimbursementCreateView.js', import.meta.url)),
'utf8'
@@ -70,6 +74,47 @@ test('documents center reloads immediately when entered or clicked again', () =>
assert.match(appShellComposable, /reloadDocumentCenterRequests,/)
})
test('documents center uses the full request list instead of the global date-filtered list', () => {
assert.match(
appShellRouteView,
/<DocumentsCenterView[\s\S]*:filtered-requests="requests"[\s\S]*:has-data="requests\.length > 0"/
)
assert.doesNotMatch(
appShellRouteView,
/<DocumentsCenterView[\s\S]*:filtered-requests="filteredRequests"/
)
})
test('workbench summary merges approval inbox requests without polluting document center rows', () => {
assert.match(appShellComposable, /import \{ fetchAllApprovalExpenseClaims, fetchExpenseClaimDetail \} from '\.\.\/services\/reimbursements\.js'/)
assert.match(appShellComposable, /const workbenchApprovalRequests = ref\(\[\]\)/)
assert.match(appShellComposable, /async function reloadWorkbenchApprovalRequests\(\)/)
assert.match(appShellComposable, /async function reloadWorkbenchRequests\(\)/)
assert.match(appShellComposable, /fetchAllApprovalExpenseClaims\(\)/)
assert.match(appShellComposable, /payload\.map\(\(item\) => mapExpenseClaimToRequest\(item\)\)/)
assert.match(appShellComposable, /Promise\.all\(\[[\s\S]*reloadRequests\(\{ silent: true \}\),[\s\S]*reloadWorkbenchApprovalRequests\(\)[\s\S]*\]\)/)
assert.match(appShellComposable, /if \(view === 'workbench'\) \{[\s\S]*void reloadWorkbenchRequests\(\)/)
assert.match(appShellComposable, /const workbenchRequests = computed\(\(\) =>[\s\S]*mergeWorkbenchRequests\(requests\.value, workbenchApprovalRequests\.value\)/)
assert.match(appShellComposable, /buildWorkbenchSummary\(workbenchRequests\.value, currentUser\.value\)/)
assert.match(appShellRouteView, /<DocumentsCenterView[\s\S]*:filtered-requests="requests"/)
assert.doesNotMatch(appShellRouteView, /<DocumentsCenterView[\s\S]*workbenchRequests/)
})
test('workbench progress refreshes after homepage create or detail updates', () => {
assert.match(appShellComposable, /async function handleDraftSaved\(payload = \{\}\) \{[\s\S]*await reloadWorkbenchRequests\(\)/)
assert.match(appShellComposable, /async function handleRequestUpdated\(\) \{[\s\S]*await reloadWorkbenchRequests\(\)[\s\S]*await refreshSelectedRequestDetail\(String\(route\.params\.requestId \|\| ''\)\)/)
assert.doesNotMatch(appShellComposable, /async function handleRequestUpdated\(\) \{[\s\S]*await reloadRequests\(\)[\s\S]*await refreshSelectedRequestDetail/)
})
test('workbench progress refresh is silent to avoid homepage flashing', () => {
assert.match(requestsComposable, /async function reload\(options = \{\}\) \{[\s\S]*const silent = Boolean\(options\?\.silent\)/)
assert.match(requestsComposable, /if \(!silent\) \{[\s\S]*loading\.value = true[\s\S]*error\.value = ''[\s\S]*\}/)
assert.match(requestsComposable, /catch \(nextError\) \{[\s\S]*if \(!silent\) \{[\s\S]*requests\.value = \[\][\s\S]*\}/)
assert.match(requestsComposable, /finally \{[\s\S]*if \(!silent\) \{[\s\S]*loading\.value = false[\s\S]*\}/)
assert.match(appShellComposable, /async function reloadWorkbenchRequests\(\) \{[\s\S]*reloadRequests\(\{ silent: true \}\)/)
assert.match(appShellComposable, /async function reloadDocumentCenterRequests\(\) \{[\s\S]*return reloadRequests\(\)/)
})
test('document detail navigation preserves document center list query', () => {
assert.match(
appShellComposable,
@@ -86,12 +131,12 @@ test('document detail navigation preserves document center list query', () => {
})
test('document detail refreshes claim detail instead of relying on stale list cache', () => {
assert.match(appShellComposable, /import \{ fetchExpenseClaimDetail \} from '\.\.\/services\/reimbursements\.js'/)
assert.match(appShellComposable, /import \{ fetchAllApprovalExpenseClaims, fetchExpenseClaimDetail \} from '\.\.\/services\/reimbursements\.js'/)
assert.match(appShellComposable, /import \{ mapExpenseClaimToRequest, useRequests \} from '\.\/useRequests\.js'/)
assert.match(appShellComposable, /const snapshot = normalizeRequestForUi\(selectedRequestSnapshot\.value\)[\s\S]*if \(isSameRequestIdentity\(snapshot, requestId\)\) \{[\s\S]*return snapshot/)
assert.match(appShellComposable, /async function refreshSelectedRequestDetail\(requestOrId = selectedRequestSnapshot\.value\) \{[\s\S]*fetchExpenseClaimDetail\(lookupId\)[\s\S]*mapExpenseClaimToRequest\(payload\)[\s\S]*upsertRequestSnapshot\(mappedRequest\)/)
assert.match(appShellComposable, /function openRequestDetail\(request, options = \{\}\) \{[\s\S]*void refreshSelectedRequestDetail\(request\)/)
assert.match(appShellComposable, /async function handleRequestUpdated\(\) \{[\s\S]*await reloadRequests\(\)[\s\S]*await refreshSelectedRequestDetail\(String\(route\.params\.requestId \|\| ''\)\)/)
assert.match(appShellComposable, /async function handleRequestUpdated\(\) \{[\s\S]*await reloadWorkbenchRequests\(\)[\s\S]*await refreshSelectedRequestDetail\(String\(route\.params\.requestId \|\| ''\)\)/)
assert.match(appShellComposable, /route\.name === 'app-document-detail'[\s\S]*void refreshSelectedRequestDetail\(String\(route\.params\.requestId \|\| ''\)\)/)
})

View File

@@ -16,6 +16,14 @@ const documentListSharedStyles = readFileSync(
fileURLToPath(new URL('../src/assets/styles/components/document-list-shared.css', import.meta.url)),
'utf8'
)
const reimbursementService = readFileSync(
fileURLToPath(new URL('../src/services/reimbursements.js', import.meta.url)),
'utf8'
)
const requestsComposable = readFileSync(
fileURLToPath(new URL('../src/composables/useRequests.js', import.meta.url)),
'utf8'
)
test('documents center keeps only the top scope tabs and renders status as a dropdown filter', () => {
assert.match(documentsCenterView, /<nav class="status-tabs document-scope-tabs"/)
@@ -134,6 +142,17 @@ test('documents center refresh token reloads supporting approval and archive row
assert.match(documentsCenterView, /function reloadAll\(\) \{[\s\S]*emit\('reload'\)[\s\S]*void loadSupportingRows\(\)/)
})
test('documents center fetches every paginated claim page for admin-scale lists', () => {
assert.match(reimbursementService, /export function fetchAllExpenseClaims/)
assert.match(reimbursementService, /async function fetchAllExpenseClaimPages/)
assert.match(reimbursementService, /payload\.has_next/)
assert.match(requestsComposable, /import \{ fetchAllExpenseClaims \} from '\.\.\/services\/reimbursements\.js'/)
assert.match(requestsComposable, /const payload = await fetchAllExpenseClaims\(\)/)
assert.match(documentsCenterView, /fetchAllApprovalExpenseClaims/)
assert.match(documentsCenterView, /fetchAllArchivedExpenseClaims/)
assert.doesNotMatch(documentsCenterView, /REIMBURSEMENT_LIST_PREVIEW_PARAMS/)
})
test('documents center list shows created time and conditional stay time columns', () => {
assert.match(documentsCenterView, /import \{[\s\S]*formatDocumentListTime[\s\S]*resolveDocumentStayTimeDisplay[\s\S]*\} from '..\/utils\/documentCenterTime\.js'/)
assert.match(documentsCenterView, /<col class="col-created">/)
@@ -326,10 +345,9 @@ test('documents center status dropdown uses compact filter styling', () => {
assert.match(documentsCenterStyles, /\.col-created\s*\{\s*width:\s*10%;\s*\}/)
assert.match(documentsCenterStyles, /\.col-stay\s*\{\s*width:\s*9%;\s*\}/)
assert.match(documentsCenterStyles, /\.col-initiator\s*\{\s*width:\s*8%;\s*\}/)
assert.match(documentsCenterStyles, /\.document-status-filter\s*\{[\s\S]*display:\s*inline-flex;/)
assert.match(documentsCenterStyles, /\.document-status-filter\s*\{[\s\S]*min-height:\s*38px;/)
assert.match(documentsCenterStyles, /\.status-filter-trigger\s*\{[\s\S]*min-width:\s*154px;/)
assert.match(documentsCenterStyles, /\.status-filter-menu\s*\{[\s\S]*min-width:\s*154px;/)
assert.match(documentListSharedStyles, /\.document-status-filter\s*\{[\s\S]*display:\s*inline-flex;/)
assert.match(documentListSharedStyles, /\.document-status-filter\s*\{[\s\S]*min-height:\s*38px;/)
assert.match(documentListSharedStyles, /\.status-dropdown-filter,\s*\.status-filter-trigger,\s*\.status-filter-menu\s*\{[\s\S]*min-width:\s*154px;/)
assert.doesNotMatch(documentsCenterStyles, /\.document-state-tabs\s*\{/)
assert.doesNotMatch(documentsCenterStyles, /\.document-status-filter\s*\{[^}]*margin-top:/)
})

View File

@@ -22,6 +22,18 @@ test('expense profile modal remounts the behavior radar when opened', () => {
assert.match(modal, /scheduleRadarFrame/)
})
test('expense profile modal uses compact laptop dialog sizing', () => {
assert.match(modal, /width="min\(960px, calc\(100vw - 64px\)\)"/)
assert.match(modal, /max-height:\s*min\(580px, calc\(100dvh - 176px\)\)/)
assert.match(
modal,
/@media \(min-width: 861px\) and \(max-width: 1440px\),\s*\n\s*\(min-width: 861px\) and \(max-height: 820px\)/
)
assert.match(modal, /width:\s*min\(900px, calc\(100vw - 96px\)\) !important;/)
assert.match(modal, /max-height:\s*min\(520px, calc\(100dvh - 152px\)\)/)
assert.match(modal, /\.profile-radar-chart \{[\s\S]*height:\s*248px;/)
})
test('radar chart uses the shared echarts lifecycle and enables entrance animation', () => {
assert.match(radarChart, /import \{ useEcharts \} from '\.\.\/\.\.\/composables\/useEcharts\.js'/)
assert.match(radarChart, /useEcharts\(chartElement, chartOptions\)/)

View File

@@ -30,6 +30,15 @@ test('expense stats detail modal exposes distribution, processing time and opera
assert.match(modal, /resolveDistributionColor/)
assert.match(modal, /processingRows/)
assert.match(modal, /operationRows/)
assert.match(modal, /width="min\(900px, calc\(100vw - 64px\)\)"/)
assert.match(modal, /max-height:\s*min\(580px, calc\(100dvh - 176px\)\)/)
assert.match(
modal,
/@media \(min-width: 861px\) and \(max-width: 1440px\),\s*\n\s*\(min-width: 861px\) and \(max-height: 820px\)/
)
assert.match(modal, /width:\s*min\(860px, calc\(100vw - 96px\)\) !important;/)
assert.match(modal, /max-height:\s*min\(520px, calc\(100dvh - 152px\)\)/)
assert.match(modal, /\.expense-distribution-donut :deep\(\.donut-body\) \{[\s\S]*height:\s*170px;/)
assert.doesNotMatch(modal, /--expense-detail-percent/)
assert.doesNotMatch(modal, /expense-distribution-track/)
assert.match(modal, /统计口径来自当前工作台已加载的个人单据/)

View File

@@ -13,6 +13,14 @@ const workbench = readFileSync(
fileURLToPath(new URL('../src/components/business/PersonalWorkbench.vue', import.meta.url)),
'utf8'
)
const workbenchProgressPanel = readFileSync(
fileURLToPath(new URL('../src/components/business/PersonalWorkbenchProgressPanel.vue', import.meta.url)),
'utf8'
)
const workbenchProgressStyles = readFileSync(
fileURLToPath(new URL('../src/assets/styles/components/personal-workbench-progress.css', import.meta.url)),
'utf8'
)
const workbenchStyles = readFileSync(
fileURLToPath(new URL('../src/assets/styles/components/personal-workbench.css', import.meta.url)),
'utf8'
@@ -31,7 +39,7 @@ const workbenchInsightStyles = readFileSync(
'utf8'
)
const heroBackgroundAsset = fileURLToPath(
new URL('../src/assets/personal-workbench-hero-bg-theme-base.webp', import.meta.url)
new URL('../src/assets/images/hero-3d-banner.png', import.meta.url)
)
const capabilityGlassAsset = fileURLToPath(
new URL('../src/assets/personal-workbench-card-glass-capability.webp', import.meta.url)
@@ -43,8 +51,9 @@ const panelGlassAsset = fileURLToPath(
test('workbench assistant greets the current employee without the old helper tag', () => {
assert.doesNotMatch(workbench, /assistant-tag/)
assert.doesNotMatch(workbench, /AI 报销助手/)
assert.match(workbench, /嗨,\{\{ displayUserName \}\},我是您的 <span>AI 费用助手<\/span>/)
assert.match(workbench, /placeholder="请输入费用申请、报销问题、预算查询或制度问答\.\.\."/)
assert.match(workbench, /\{\{ typedTitlePrefix \}\}<span v-if="titleTypingDone">小财管家<\/span>/)
assert.match(workbench, /const heroTitleText = computed\(\(\) => `嗨,\$\{displayUserName\.value\},我是您的 `\)/)
assert.match(workbench, /placeholder="一次性描述申请、报销和附件处理事项,小财管家会先拆解再执行\.\.\."/)
assert.match(workbench, /const displayUserName = computed/)
assert.match(workbench, /user\.name/)
})
@@ -83,16 +92,16 @@ test('workbench capability cards keep user-entered context only', () => {
})
test('workbench hero uses theme-tintable background image', () => {
assert.match(workbench, /personal-workbench-hero-bg-theme-base\.webp/)
assert.doesNotMatch(workbench, /personal-workbench-hero-bg-theme-base\.png/)
assert.match(workbench, /hero-3d-banner\.png/)
assert.doesNotMatch(workbench, /personal-workbench-hero-bg-theme-base\.(webp|png)/)
assert.match(workbench, /--assistant-bg-image.*workbenchHeroBackground/)
assert.match(workbenchStyles, /--assistant-theme-tint:[\s\S]*--theme-primary-rgb/)
assert.match(workbenchStyles, /var\(--assistant-bg-image\) var\(--assistant-bg-position\) \/ var\(--assistant-bg-size\) no-repeat/)
assert.match(workbenchStyles, /background-blend-mode:\s*normal,\s*color,\s*luminosity;/)
assert.match(workbenchStyles, /\.assistant-hero::after\s*\{[\s\S]*content:\s*none;/)
assert.match(workbenchResponsiveStyles, /--assistant-bg-position:\s*68% center;/)
assert.match(workbenchStyles, /url\("\.\.\/\.\.\/images\/workbench-hero-right-bg\.png"\) var\(--assistant-bg-position\) \/ var\(--assistant-decor-width\) auto no-repeat/)
assert.match(workbenchStyles, /\.assistant-hero::after\s*\{[\s\S]*content:\s*"";/)
assert.match(workbenchResponsiveStyles, /--assistant-bg-position:\s*right center;/)
assert.doesNotMatch(workbenchResponsiveStyles, /homepage_backgraound/)
assert.ok(statSync(heroBackgroundAsset).size < 120 * 1024)
assert.ok(statSync(heroBackgroundAsset).size > 1024)
assert.ok(statSync(heroBackgroundAsset).size < 600 * 1024)
})
test('workbench cards use layered glass material instead of texture-led cards', () => {
@@ -103,13 +112,11 @@ test('workbench cards use layered glass material instead of texture-led cards',
assert.match(workbenchGlassStyles, /--workbench-glass-highlight:/)
assert.match(workbenchGlassStyles, /--workbench-glass-noise-opacity:\s*0\.012;/)
assert.match(workbenchGlassStyles, /--workbench-glass-blur:\s*blur\(18px\) saturate\(1\.28\);/)
assert.match(workbenchGlassStyles, /\.capability-card\s*\{[\s\S]*background-color:\s*rgba\(255,\s*255,\s*255,\s*0\.64\);[\s\S]*backdrop-filter:\s*var\(--workbench-glass-blur\)/)
assert.match(workbenchGlassStyles, /\.workbench-card\s*\{[\s\S]*background-color:\s*rgba\(255,\s*255,\s*255,\s*0\.66\);[\s\S]*backdrop-filter:\s*var\(--workbench-glass-blur\)/)
assert.match(workbenchGlassStyles, /\.capability-card\s*\{[\s\S]*background:\s*rgba\(255,\s*255,\s*255,\s*0\.96\);[\s\S]*backdrop-filter:\s*blur\(12px\) saturate\(150%\)/)
assert.match(workbenchGlassStyles, /\.workbench-card\s*\{[\s\S]*background:\s*rgba\(255,\s*255,\s*255,\s*0\.96\);[\s\S]*backdrop-filter:\s*blur\(12px\) saturate\(150%\)/)
assert.match(workbenchGlassStyles, /\.capability-card::before,[\s\S]*\.capability-card::after/)
assert.match(workbenchGlassStyles, /\.capability-card::before\s*\{[\s\S]*var\(--workbench-capability-bg-image\) 0 0 \/ var\(--workbench-capability-tile-size\) repeat;[\s\S]*opacity:\s*var\(--workbench-glass-noise-opacity\);/)
assert.match(workbenchGlassStyles, /\.workbench-card::before\s*\{[\s\S]*var\(--workbench-panel-bg-image\) 0 0 \/ var\(--workbench-panel-tile-size\) repeat;[\s\S]*opacity:\s*calc\(var\(--workbench-glass-noise-opacity\) \* 0\.8\);/)
assert.match(workbenchGlassStyles, /\.capability-card::after\s*\{[\s\S]*var\(--workbench-glass-highlight\)/)
assert.match(workbenchGlassStyles, /\.workbench-card::after\s*\{[\s\S]*var\(--workbench-glass-highlight\)/)
assert.match(workbenchGlassStyles, /\.workbench-card::before,[\s\S]*\.workbench-card::after\s*\{[\s\S]*display:\s*none !important;/)
assert.doesNotMatch(workbenchGlassStyles, /\.capability-card::after\s*\{[^}]*radial-gradient/)
assert.doesNotMatch(workbenchGlassStyles, /\.workbench-card::after\s*\{[^}]*radial-gradient/)
assert.match(workbenchGlassStyles, /\.workbench-card > \*\s*\{[\s\S]*z-index:\s*1;/)
@@ -143,29 +150,67 @@ test('workbench submit shows intent recognition feedback before assistant opens'
assert.match(workbench, /:readonly="isComposerPending"/)
})
test('workbench progress rows show update time first', () => {
assert.match(workbench, /class="progress-time"/)
assert.match(workbench, /class="progress-type"/)
assert.match(workbench, /费用类型/)
assert.match(workbench, /\{\{ item\.expenseTypeLabel \|\| '其他费用' \}\}/)
test('workbench document progress has range filter, document types and empty state', () => {
assert.match(workbench, /import PersonalWorkbenchProgressPanel from '\.\/PersonalWorkbenchProgressPanel\.vue'/)
assert.match(workbench, /<PersonalWorkbenchProgressPanel[\s\S]*:progress-items="workbenchSummary\.progressItems \|\| \[\]"[\s\S]*@open-target="openWorkbenchTarget"/)
assert.match(workbenchProgressPanel, /<h2>单据进度<\/h2>/)
assert.match(workbenchProgressPanel, /personal-workbench-progress\.css/)
assert.match(workbenchProgressPanel, /EnterpriseSelect/)
assert.match(workbenchProgressPanel, /近10日/)
assert.match(workbenchProgressPanel, /近30日/)
assert.match(workbenchProgressPanel, /近3个月/)
assert.match(workbenchProgressPanel, /class="progress-range-control"[\s\S]*@click\.stop[\s\S]*@mousedown\.stop[\s\S]*@pointerdown\.stop/)
assert.match(workbenchProgressPanel, /:teleported="true"/)
assert.match(workbenchProgressPanel, /@click="handleProgressItemClick\(\$event, item\)"/)
assert.match(workbenchProgressPanel, /function handleProgressItemClick\(event, item\)/)
assert.match(workbenchProgressPanel, /return true/)
assert.match(workbenchProgressPanel, /class="progress-time"/)
assert.match(workbenchProgressPanel, /class="progress-applicant"/)
assert.match(workbenchProgressPanel, /class="progress-table-shell"/)
assert.match(workbenchProgressPanel, />\u66f4\u65b0\u65f6\u95f4<\/span>/)
assert.doesNotMatch(workbenchProgressPanel, /\u5355\u636e\u52a8\u6001/)
assert.doesNotMatch(workbenchProgressPanel, /time-capsule/)
assert.doesNotMatch(workbenchProgressPanel, /<small>\u7533\u8bf7\u4eba<\/small>/)
assert.match(workbenchProgressPanel, /\{\{ item\.applicantLabel \|\| '待补充' \}\}/)
assert.match(workbenchProgressPanel, /class="progress-type"/)
assert.match(workbenchProgressPanel, /\{\{ item\.documentTypeLabel \}\} · \{\{ item\.expenseTypeLabel \|\| '其他费用' \}\}/)
assert.match(workbenchProgressPanel, /当前范围暂无单据/)
assert.match(workbenchProgressPanel, /没有申请单或报销单进度/)
assert.match(workbench, /source:\s*'workbench'/)
assert.match(workbench, /returnTo:\s*'workbench'/)
assert.match(workbench, /has-long-duration-divider/)
assert.match(workbench, /hasLongDurationDivider/)
assert.match(workbench, /const LONG_DURATION_DAYS = 10/)
assert.match(workbench, /isLongDurationProgress\(item\?\.updatedAt\)/)
assert.match(workbench, /<time :datetime="item\.updatedAt \|\| ''">\{\{ item\.displayTime \}\}<\/time>/)
assert.match(workbench, /displayTime: formatProgressTime\(item\?\.updatedAt\)/)
assert.match(workbench, /function formatProgressTime\(value\)/)
assert.doesNotMatch(workbench, />全部进度/)
assert.match(workbenchStyles, /\.progress-row\s*\{[\s\S]*grid-template-columns:\s*minmax\(78px,\s*0\.42fr\)[\s\S]*minmax\(84px,\s*0\.42fr\)/)
assert.doesNotMatch(workbenchProgressPanel, /has-long-duration-divider/)
assert.doesNotMatch(workbenchProgressPanel, /LONG_DURATION_DAYS/)
assert.doesNotMatch(workbenchProgressPanel, /10日以上/)
assert.doesNotMatch(workbenchStyles, /10日以上/)
assert.doesNotMatch(workbenchStyles, /has-long-duration-divider/)
assert.doesNotMatch(workbench, />费用进度</)
assert.match(workbenchProgressPanel, /<time :datetime="item\.updatedAt \|\| ''">\{\{ item\.displayTime \}\}<\/time>/)
assert.match(workbenchProgressPanel, /function formatProgressTime\(value\)/)
assert.match(workbenchStyles, /\.progress-row\s*\{[\s\S]*grid-template-columns:\s*minmax\(118px,\s*0\.58fr\)[\s\S]*minmax\(84px,\s*0\.42fr\)/)
assert.match(workbenchStyles, /\.progress-type\s*\{[\s\S]*justify-self:\s*stretch;[\s\S]*justify-items:\s*center;[\s\S]*text-align:\s*center;/)
assert.match(workbenchStyles, /\.progress-type strong\s*\{[\s\S]*justify-content:\s*center;/)
assert.match(workbenchStyles, /\.progress-type strong\s*\{[\s\S]*var\(--workbench-primary-active\)/)
assert.match(workbenchStyles, /\.progress-row\.has-long-duration-divider::before\s*\{[\s\S]*content:\s*"10日以上"/)
assert.match(workbenchStyles, /\.progress-row\.has-long-duration-divider::before\s*\{[\s\S]*left:\s*50%;[\s\S]*transform:\s*translateX\(-50%\);/)
assert.match(workbenchStyles, /\.progress-row\.has-long-duration-divider::before\s*\{[\s\S]*color:\s*var\(--theme-primary-active\);/)
assert.match(workbenchStyles, /\.progress-row\.has-long-duration-divider::after\s*\{[\s\S]*rgba\(var\(--theme-primary-rgb/)
assert.match(workbenchStyles, /\.progress-list\s*\{[\s\S]*overflow-y:\s*auto;/)
assert.match(workbenchStyles, /\.progress-empty-state\s*\{[\s\S]*border:\s*1px dashed/)
assert.match(workbenchStyles, /\.progress-range-select\s*\{[\s\S]*width:\s*124px;/)
assert.match(workbenchProgressStyles, /\.progress-panel\s*\{[\s\S]*grid-template-rows:\s*auto minmax\(0,\s*1fr\);/)
assert.match(workbenchProgressStyles, /--progress-table-columns:[\s\S]*minmax\(96px,\s*0\.44fr\);/)
assert.match(workbenchProgressStyles, /\.progress-table-shell\s*\{[\s\S]*grid-template-rows:\s*36px minmax\(0,\s*1fr\);[\s\S]*overflow:\s*hidden;/)
assert.match(workbenchProgressStyles, /\.progress-table-header\s*\{[\s\S]*grid-template-columns:\s*var\(--progress-table-columns\);/)
assert.match(workbenchProgressStyles, /\.progress-table-header\s*\{[\s\S]*height:\s*36px;[\s\S]*max-height:\s*36px;[\s\S]*overflow:\s*hidden;/)
assert.match(workbenchProgressStyles, /\.header-cell\s*\{[\s\S]*justify-content:\s*center;[\s\S]*white-space:\s*nowrap;/)
assert.match(workbenchProgressStyles, /\.progress-list\s*\{[\s\S]*grid-auto-rows:\s*minmax\(76px,\s*auto\)/)
assert.match(workbenchProgressStyles, /\.progress-row\s*\{[\s\S]*grid-template-columns:\s*var\(--progress-table-columns\);[\s\S]*gap:\s*8px;[\s\S]*min-height:\s*76px;[\s\S]*text-align:\s*center;/)
assert.match(workbenchProgressStyles, /\.progress-time-wrapper\s*\{[\s\S]*justify-content:\s*center;/)
assert.match(workbenchProgressStyles, /\.progress-identity\s*\{[\s\S]*align-items:\s*center;[\s\S]*text-align:\s*center;/)
assert.match(workbenchProgressStyles, /\.progress-applicant\s*\{[\s\S]*justify-items:\s*center;/)
assert.match(workbenchProgressStyles, /\.progress-result\s*\{[\s\S]*justify-self:\s*stretch;[\s\S]*justify-content:\s*center;[\s\S]*justify-items:\s*center;/)
assert.match(workbenchProgressStyles, /\.progress-result strong\s*\{[\s\S]*width:\s*100%;[\s\S]*text-align:\s*center;/)
assert.match(workbenchProgressStyles, /\.progress-steps\s*\{[\s\S]*justify-items:\s*stretch;/)
assert.match(workbenchProgressStyles, /\.progress-step\s*\{[\s\S]*justify-items:\s*center;/)
assert.match(workbenchProgressStyles, /\.progress-range-control\s*\{[\s\S]*z-index:\s*6;/)
assert.match(workbenchProgressStyles, /\.progress-range-select:deep\(\.el-select__wrapper\)/)
assert.match(workbenchProgressStyles, /\.progress-empty-state\s*\{[\s\S]*border:\s*1px dashed/)
assert.match(workbenchStyles, /\.progress-time\s*\{[\s\S]*color:\s*var\(--workbench-muted\);/)
assert.match(workbenchResponsiveStyles, /grid-template-areas:[\s\S]*"time identity result"[\s\S]*"type type type"[\s\S]*"steps steps steps"/)
assert.match(workbenchResponsiveStyles, /\.progress-type\s*\{[\s\S]*grid-area:\s*type;/)

View File

@@ -10,6 +10,7 @@ import {
const CREATE_APPLICATION = '\u521b\u5efa\u7533\u8bf7'
const DIRECT_MANAGER_APPROVAL = '\u76f4\u5c5e\u9886\u5bfc\u5ba1\u6279'
const BUDGET_MANAGER_APPROVAL = '\u9884\u7b97\u7ba1\u7406\u8005\u5ba1\u6279'
const FINANCE_APPROVAL = '\u8d22\u52a1\u5ba1\u6279'
const APPROVAL_COMPLETED = '\u5ba1\u6279\u5b8c\u6210'
const APPLICATION_LINK_STATUS = '\u5173\u8054\u5355\u636e\u72b6\u6001'
const APPLICATION_ARCHIVE = '\u7533\u8bf7\u5f52\u6863'
@@ -21,6 +22,7 @@ const ARCHIVED = '\u5df2\u5f52\u6863'
const WAIT_LEADER_LI_APPROVAL = '\u7b49\u5f85 Leader Li \u6279\u590d'
const WAIT_BUDGET_ZHAO_APPROVAL = '\u7b49\u5f85 \u8d75\u9884\u7b97 \u6279\u590d'
const WAIT_BUDGET_P8_EXECUTIVE_APPROVAL = '\u7b49\u5f85 P8 Executive \u6279\u590d'
const WAIT_FINANCE_FIONA_APPROVAL = '\u7b49\u5f85 Fiona Finance \u6279\u590d'
const LEADER_RETURNED_STATUS = '\u9886\u5bfc\u5df2\u9000\u56de\uff0c\u5f85\u91cd\u65b0\u63d0\u4ea4'
test('claim mapper exposes employee identifier for reviewer risk profile lookup', () => {
@@ -158,7 +160,7 @@ test('application claims are mapped as application documents', () => {
assert.equal(request.expenseTableSummary, '预计金额已随申请提交')
assert.deepEqual(
request.progressSteps.map((step) => step.label),
[CREATE_APPLICATION, WAIT_LEADER_LI_APPROVAL, APPROVAL_COMPLETED, APPLICATION_LINK_STATUS, ARCHIVED]
[CREATE_APPLICATION, WAIT_LEADER_LI_APPROVAL, APPLICATION_LINK_STATUS, ARCHIVED]
)
assert.equal(request.progressSteps.some((step) => step.label === 'AI预审'), false)
assert.equal(request.progressSteps.some((step) => step.label === '财务审批'), false)
@@ -252,7 +254,7 @@ test('application claims wait for department P8 budget monitor after leader appr
assert.deepEqual(
request.progressSteps.map((step) => step.label),
[CREATE_APPLICATION, DIRECT_MANAGER_APPROVAL, WAIT_BUDGET_ZHAO_APPROVAL, APPROVAL_COMPLETED, APPLICATION_LINK_STATUS, ARCHIVED]
[CREATE_APPLICATION, DIRECT_MANAGER_APPROVAL, WAIT_BUDGET_ZHAO_APPROVAL, APPLICATION_LINK_STATUS, ARCHIVED]
)
assert.equal(request.progressSteps.find((step) => step.label === WAIT_BUDGET_ZHAO_APPROVAL)?.current, true)
assert.equal(request.progressSteps.find((step) => step.label === DIRECT_MANAGER_APPROVAL)?.time, 'Leader Li通过')
@@ -295,7 +297,7 @@ test('application budget wait label uses claim-level budget approver snapshot',
assert.equal(request.budgetApproverName, 'P8 Executive')
assert.deepEqual(
request.progressSteps.map((step) => step.label),
[CREATE_APPLICATION, DIRECT_MANAGER_APPROVAL, WAIT_BUDGET_P8_EXECUTIVE_APPROVAL, APPROVAL_COMPLETED, APPLICATION_LINK_STATUS, ARCHIVED]
[CREATE_APPLICATION, DIRECT_MANAGER_APPROVAL, WAIT_BUDGET_P8_EXECUTIVE_APPROVAL, APPLICATION_LINK_STATUS, ARCHIVED]
)
assert.equal(request.progressSteps.find((step) => step.label === WAIT_BUDGET_P8_EXECUTIVE_APPROVAL)?.current, true)
})
@@ -391,7 +393,7 @@ test('approved application claims complete after budget approval', () => {
assert.equal(request.workflowNode, APPLICATION_LINK_STATUS)
assert.deepEqual(
request.progressSteps.map((step) => step.label),
[CREATE_APPLICATION, DIRECT_MANAGER_APPROVAL, BUDGET_MANAGER_APPROVAL, APPROVAL_COMPLETED, APPLICATION_LINK_STATUS, ARCHIVED]
[CREATE_APPLICATION, DIRECT_MANAGER_APPROVAL, BUDGET_MANAGER_APPROVAL, APPLICATION_LINK_STATUS, ARCHIVED]
)
assert.equal(request.progressSteps.find((step) => step.label === APPLICATION_LINK_STATUS)?.current, true)
assert.equal(request.progressSteps.find((step) => step.label === APPLICATION_LINK_STATUS)?.time, '未关联')
@@ -417,15 +419,40 @@ test('application claims hide budget step when leader approval also covers budge
created_at: '2026-05-25T01:30:00.000Z',
updated_at: '2026-05-25T03:00:00.000Z',
status: 'approved',
approval_stage: APPROVAL_COMPLETED,
approval_stage: APPLICATION_LINK_STATUS,
risk_flags_json: [
{
source: 'approval_routing',
event_type: 'expense_application_route_decision',
requires_budget_review: true,
route: 'budget_manager',
budget_result: {
metrics: {
after_usage_rate: '99.27',
claim_amount_ratio: '1.27',
over_budget_amount: '0.00'
}
},
created_at: '2026-05-25T03:00:00.000Z'
},
{
source: 'manual_approval',
event_type: 'expense_application_approval',
operator: '李预算经理',
previous_approval_stage: DIRECT_MANAGER_APPROVAL,
next_approval_stage: APPROVAL_COMPLETED,
next_approval_stage: APPLICATION_LINK_STATUS,
budget_approval_merged: true,
route_decision: {
requires_budget_review: true,
route: 'budget_manager',
budget_result: {
metrics: {
after_usage_rate: '99.27',
claim_amount_ratio: '1.27',
over_budget_amount: '0.00'
}
}
},
created_at: '2026-05-25T03:00:00.000Z'
}
],
@@ -434,7 +461,7 @@ test('application claims hide budget step when leader approval also covers budge
assert.deepEqual(
request.progressSteps.map((step) => step.label),
[CREATE_APPLICATION, DIRECT_MANAGER_APPROVAL, APPROVAL_COMPLETED, APPLICATION_LINK_STATUS, ARCHIVED]
[CREATE_APPLICATION, DIRECT_MANAGER_APPROVAL, APPLICATION_LINK_STATUS, ARCHIVED]
)
assert.equal(request.progressSteps.find((step) => step.label === APPLICATION_LINK_STATUS)?.current, true)
assert.equal(request.progressSteps.find((step) => step.label === ARCHIVED)?.done, false)
@@ -486,7 +513,7 @@ test('approved application claims hide budget step when dynamic route skipped bu
assert.deepEqual(
request.progressSteps.map((step) => step.label),
[CREATE_APPLICATION, DIRECT_MANAGER_APPROVAL, APPROVAL_COMPLETED, APPLICATION_LINK_STATUS, ARCHIVED]
[CREATE_APPLICATION, DIRECT_MANAGER_APPROVAL, APPLICATION_LINK_STATUS, ARCHIVED]
)
assert.equal(request.progressSteps.find((step) => step.label === APPLICATION_LINK_STATUS)?.current, true)
assert.equal(request.progressSteps.find((step) => step.label === ARCHIVED)?.done, false)
@@ -494,6 +521,69 @@ test('approved application claims hide budget step when dynamic route skipped bu
assert.equal(request.progressSteps.find((step) => step.label === DIRECT_MANAGER_APPROVAL)?.time, 'Leader Li通过')
})
test('approved application claims hide stale budget route below 90 percent threshold', () => {
const request = mapExpenseClaimToRequest({
id: 'claim-application-stale-budget-route',
claim_no: 'APP-20260525-STALE-BUDGET',
employee_name: '张三',
department_name: '交付部',
manager_name: 'Leader Li',
expense_type: 'travel_application',
reason: 'Project onsite support',
location: 'Shanghai',
amount: 8500,
invoice_count: 0,
occurred_at: '2026-05-25T00:00:00.000Z',
submitted_at: '2026-05-25T02:00:00.000Z',
created_at: '2026-05-25T01:30:00.000Z',
updated_at: '2026-05-25T03:00:00.000Z',
status: 'approved',
approval_stage: APPROVAL_COMPLETED,
risk_flags_json: [
{
source: 'approval_routing',
event_type: 'expense_application_route_decision',
requires_budget_review: true,
route: 'budget_manager',
budget_result: {
metrics: {
after_usage_rate: '85.00',
claim_amount_ratio: '85.00',
over_budget_amount: '0.00'
}
},
created_at: '2026-05-25T03:00:00.000Z'
},
{
source: 'manual_approval',
event_type: 'expense_application_approval',
operator: 'Leader Li',
previous_approval_stage: DIRECT_MANAGER_APPROVAL,
next_approval_stage: BUDGET_MANAGER_APPROVAL,
route_decision: {
requires_budget_review: true,
route: 'budget_manager',
budget_result: {
metrics: {
after_usage_rate: '85.00',
claim_amount_ratio: '85.00',
over_budget_amount: '0.00'
}
}
},
created_at: '2026-05-25T03:00:00.000Z'
}
],
items: []
})
assert.deepEqual(
request.progressSteps.map((step) => step.label),
[CREATE_APPLICATION, DIRECT_MANAGER_APPROVAL, APPLICATION_LINK_STATUS, ARCHIVED]
)
assert.equal(request.progressSteps.some((step) => step.label === BUDGET_MANAGER_APPROVAL), false)
})
test('approved application claims show linked reimbursement status before archive', () => {
const request = mapExpenseClaimToRequest({
id: 'claim-application-linked-draft',
@@ -566,7 +656,7 @@ test('application claims are archived only after linked reimbursement is paid',
assert.equal(request.workflowNode, APPLICATION_ARCHIVE)
assert.deepEqual(
request.progressSteps.map((step) => step.label),
[CREATE_APPLICATION, DIRECT_MANAGER_APPROVAL, APPROVAL_COMPLETED, APPLICATION_LINK_STATUS, ARCHIVED]
[CREATE_APPLICATION, DIRECT_MANAGER_APPROVAL, APPLICATION_LINK_STATUS, ARCHIVED]
)
assert.equal(request.progressSteps.every((step) => step.done), true)
assert.equal(request.secondaryStatusValue, '已归档')
@@ -582,6 +672,8 @@ test('progress steps show approval operator time and current stay duration', ()
claim_no: 'EXP-202605-001',
employee_name: '张三',
department_name: '市场部',
finance_owner_name: 'Wang Finance Group',
finance_approver_name: 'Fiona Finance',
expense_type: 'transport',
reason: '交通报销',
location: '上海',
@@ -592,21 +684,21 @@ test('progress steps show approval operator time and current stay duration', ()
created_at: '2026-05-20T01:30:00.000Z',
updated_at: '2026-05-20T03:30:00.000Z',
status: 'submitted',
approval_stage: '财务审批',
approval_stage: FINANCE_APPROVAL,
risk_flags_json: [
{
source: 'manual_approval',
operator: '李经理',
previous_approval_stage: '直属领导审批',
next_approval_stage: '财务审批',
previous_approval_stage: DIRECT_MANAGER_APPROVAL,
next_approval_stage: FINANCE_APPROVAL,
created_at: '2026-05-20T03:30:00.000Z'
}
],
items: []
})
const leaderStep = request.progressSteps.find((step) => step.label === '直属领导审批')
const financeStep = request.progressSteps.find((step) => step.label === '财务审批')
const leaderStep = request.progressSteps.find((step) => step.label === DIRECT_MANAGER_APPROVAL)
const financeStep = request.progressSteps.find((step) => step.rawLabel === FINANCE_APPROVAL)
const firstStep = request.progressSteps[0]
assert.equal(request.riskSummary, '无')
@@ -615,6 +707,10 @@ test('progress steps show approval operator time and current stay duration', ()
assert.match(leaderStep.detail, /2026-05-20/)
assert.match(leaderStep.title, /李经理审批通过/)
assert.equal(request.progressSteps.some((step) => step.label === 'AI预审'), false)
assert.equal(request.financeOwnerName, 'Wang Finance Group')
assert.equal(request.financeApproverName, 'Fiona Finance')
assert.equal(financeStep.label, WAIT_FINANCE_FIONA_APPROVAL)
assert.equal(financeStep.rawLabel, FINANCE_APPROVAL)
assert.equal(financeStep.current, true)
assert.equal(financeStep.time, '停留 1小时30分钟')
} finally {
@@ -1124,6 +1220,7 @@ test('current direct manager step shows how long the claim has stayed there', ()
claim_no: 'EXP-202605-002',
employee_name: '王五',
department_name: '市场部',
manager_name: '李经理',
expense_type: 'office',
reason: '办公用品',
location: '上海',
@@ -1139,11 +1236,13 @@ test('current direct manager step shows how long the claim has stayed there', ()
items: []
})
const leaderStep = request.progressSteps.find((step) => step.label === '直属领导审批')
const leaderStep = request.progressSteps.find((step) => step.rawLabel === '直属领导审批')
const submitStep = request.progressSteps.find((step) => step.label === '待提交')
assert.equal(submitStep.time, '王五提交')
assert.match(submitStep.detail, /2026-05-20/)
assert.equal(leaderStep.label, '等待 李经理 批复')
assert.equal(leaderStep.rawLabel, '直属领导审批')
assert.equal(leaderStep.current, true)
assert.equal(leaderStep.time, '停留 3小时15分钟')
} finally {

View File

@@ -67,6 +67,7 @@ test('approval-mode detail collects leader opinion inside confirm dialog before
assert.match(detailScript, /const isCurrentDirectManagerApprover = computed/)
assert.match(detailScript, /const canProcessFinanceApprovalStage = computed/)
assert.match(detailScript, /const canProcessBudgetApprovalStage = computed/)
assert.match(detailScript, /const canProcessCurrentApprovalStage = computed/)
assert.match(detailScript, /approvalOpinionTitle/)
assert.match(detailScript, /approvalConfirmDescription/)
assert.doesNotMatch(detailScript, /approvalNextStage/)
@@ -84,9 +85,14 @@ test('approval-mode detail collects leader opinion inside confirm dialog before
/const showApplicationLeaderOpinion = computed\(\(\) => \(\s*isApplicationDocument\.value\s*&& hasLeaderApprovalEvents\.value\s*\)\)/
)
assert.match(detailScript, /isDirectManagerApprovalStage\.value\)[\s\S]*return isCurrentDirectManagerApprover\.value/)
assert.match(detailScript, /isDirectManagerApprovalStage\.value[\s\S]*&& isCurrentDirectManagerApprover\.value/)
assert.match(detailScript, /if \(isDirectManagerApprovalStage\.value\) \{[\s\S]*return isCurrentDirectManagerApprover\.value/)
assert.match(detailScript, /canProcessFinanceApprovalStage\.value/)
assert.match(detailScript, /canProcessBudgetApprovalStage\.value/)
assert.match(detailScript, /const canApproveRequest = computed\(\(\) =>\s*request\.value\.approvalKey === 'in_progress'[\s\S]*&& canProcessCurrentApprovalStage\.value\s*\)/)
assert.doesNotMatch(
detailScript,
/const canApproveRequest = computed\(\(\) =>\s*\(Boolean\(props\.approvalMode\) \|\| isApplicationDocument\.value\)/
)
assert.doesNotMatch(detailScript, /leaderApprovalReadonlyText/)
assert.match(detailScript, /resolveGeneratedDraftClaimNo/)
assert.match(detailScript, /resolveApproveErrorMessage/)

View File

@@ -1169,6 +1169,28 @@ test('draft submit validation uses expense detail date and amount when claim sum
assert.ok(!issues.some((issue) => issue.includes('报销事由未完善')))
})
test('returned application submit validation does not require expense detail rows', () => {
const issues = buildDraftBlockingIssues(
{
id: 'AP-202606060001-RETURNED',
claimNo: 'AP-202606060001-RETURNED',
documentTypeCode: 'application',
approvalKey: 'rejected',
profileName: '曹笑竹',
typeLabel: '差旅费用申请',
typeCode: 'travel_application',
reason: '支撑国网仿生产环境部署',
location: '上海',
occurredDisplay: '2026-02-20 至 2026-02-23',
amountValue: 1880
},
[]
)
assert.deepEqual(issues, [])
assert.equal(issues.includes('费用明细不能为空'), false)
})
test('transport ticket descriptions use route format and invalid format becomes risk advice', () => {
const routeItem = buildExpenseItemViewModel(
{

View File

@@ -71,6 +71,9 @@ test('workbench summary builds real user notifications and progress from request
assert.equal(summary.todoItems.length, 1)
assert.equal(summary.todoItems[0].target.id, 'claim-1')
assert.equal(summary.progressItems.length, 1)
assert.match(summary.progressItems[0].prompt, /单据进度/)
assert.doesNotMatch(summary.progressItems[0].prompt, /费用进度/)
assert.equal(summary.progressItems[0].applicantLabel, '张三')
assert.deepEqual(
summary.progressItems[0].steps.map((step) => step.label),
['创建单据', '待提交', '直属领导审批', '财务审批']
@@ -136,3 +139,65 @@ test('workbench progress keeps application document type for AP claims', () => {
['申请单', '申请单']
)
})
test('workbench progress includes application rows without backend progress steps', () => {
const summary = buildWorkbenchSummary(
[
{
id: 'AP-202606060001-NOSTEPS',
claimId: 'application-without-steps',
claimNo: 'AP-202606060001-NOSTEPS',
documentTypeCode: 'application',
person: currentUser.name,
title: '上海出差申请',
approvalKey: 'in_progress',
approvalStatus: '直属领导审批',
amount: 1880,
updatedAt: '2026-06-06T09:00:00+08:00'
}
],
currentUser
)
assert.equal(summary.progressItems.length, 1)
assert.equal(summary.progressItems[0].documentTypeLabel, '申请单')
assert.equal(summary.progressItems[0].applicantLabel, currentUser.name)
assert.deepEqual(
summary.progressItems[0].steps.map((step) => step.label),
['创建申请', '直属领导审批', '关联单据状态', '已归档']
)
assert.equal(summary.progressItems[0].steps[1].current, true)
})
test('workbench progress includes application documents assigned to current approver', () => {
const approverUser = {
name: '李经理',
username: 'manager@example.com',
roleCodes: ['approver']
}
const summary = buildWorkbenchSummary(
[
{
id: 'AP-202606060002-APPROVAL',
claimId: 'application-for-approver',
claimNo: 'AP-202606060002-APPROVAL',
documentTypeCode: 'application',
person: '张三',
managerName: '李经理',
title: '北京出差申请',
approvalKey: 'in_progress',
approvalStatus: '直属领导审批',
workflowNode: '直属领导审批',
amount: 2600,
updatedAt: '2026-06-06T11:00:00+08:00'
}
],
approverUser
)
assert.equal(summary.totalCount, 0)
assert.equal(summary.progressItems.length, 1)
assert.equal(summary.progressItems[0].documentTypeLabel, '申请单')
assert.equal(summary.progressItems[0].applicantLabel, '张三')
assert.equal(summary.progressItems[0].requestId, 'AP-202606060002-APPROVAL')
})